项目文件夹

文件
wehub-resource-sync 3e779be6f3
CI / lint (push) Failing after 13m4s
CI / test (3.11, ubuntu-latest) (push) Failing after 2m4s
CI / test (3.13, ubuntu-latest) (push) Successful in 13m30s
CI / test (3.14, ubuntu-latest) (push) Successful in 17m21s
CI / test (3.12, ubuntu-latest) (push) Successful in 17m55s
CI / discover-apps-ps (push) Successful in 1m56s
CI / test (3.9, ubuntu-latest) (push) Successful in 13m17s
CI / test (3.10, ubuntu-latest) (push) Successful in 26m21s
CI / audit (push) Successful in 13m38s
Deploy site / deploy (push) Has been cancelled
CI / test (3.14, ubuntu-24.04-arm) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:37 +08:00

1753 行
78 KiB
Bash
可执行文件

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
#!/usr/bin/env bash
# SPDX-License-Identifier: MIT
set -euo pipefail
###############################################################################
# winpodx installer (v2)
#
# Usage:
# curl -sSL https://raw.githubusercontent.com/kernalix7/winpodx/main/install.sh | bash
# or: ./install.sh [--main] [--ref TAG] [--source PATH] [--image-tar PATH]
# [--mode r|a|c|n] [--backend podman|docker|manual]
# [--no-gui] [--manual] [--skip-deps] [--win-version VER]
# [--win-iso PATH] [--storage-dir PATH] [--help]
#
# Installs winpodx to ~/.local/bin/winpodx-app/ and creates a launcher.
# Python always runs from a private venv under
# ~/.local/bin/winpodx-app/.venv ; no system-python changes.
#
# Install mode (interactive prompt, or preselect with --mode / WINPODX_MODE):
# --mode r Recommended — winpodx's recommended stack (Podman backend
# + deps); installs missing system packages via the distro
# package manager (sudo). This is the historical behaviour.
# --mode a Automatic — reuse what's already installed; only install
# what's strictly missing; pick the backend from what's
# present (prefer an already-working docker/podman).
# --mode c Custom — choose backend (podman/docker) and
# whether to include the GUI, then install accordingly.
# --mode n No — cancel without changing anything.
# (env: WINPODX_MODE=<r|a|c|n>)
# Default: prompt when a terminal is reachable — including
# `curl ... | bash` (prompts read from /dev/tty) — and 'r'
# (Recommended) when fully non-interactive (CI / cron /
# stdin+/dev/tty both absent).
#
# Backend / GUI:
# --backend BACKEND podman | docker | manual. Passed through to
# `winpodx setup --backend <x>`. (env: WINPODX_BACKEND)
# (libvirt was dropped in 0.6.0 — dockur covers device
# passthrough now, #286.)
# --verbose, -v Stream raw container logs during the Windows-boot wait.
# Default collapses the ISO download to one clean progress
# line + hides UEFI boot noise. (env: WINPODX_VERBOSE=1)
# --no-gui Headless / CLI-only: skip installing PySide6 into the
# venv. Everything else still installs.
# (env: WINPODX_NO_GUI=1)
#
# Version selection (default: latest GitHub release):
# --main Install from git main HEAD (development, may be unstable).
# (env: WINPODX_REF=main)
# --ref TAG Install a specific tag/branch/commit.
# (env: WINPODX_REF=<ref>)
#
# Local-path options (for offline / air-gapped installs):
# --source PATH Copy winpodx from PATH instead of git clone.
# (env: WINPODX_SOURCE)
# --image-tar PATH Preload Windows container image from PATH via
# `podman load -i` (or `docker load -i`).
# (env: WINPODX_IMAGE_TAR)
# --skip-deps Skip the distro dependency install phase.
# Fails early if required tools aren't already present.
# (env: WINPODX_SKIP_DEPS=1)
# --win-version VER Windows edition for fresh installs
# (11 | 10 | ltsc11 | ltsc10 | iot11 | tiny11 | tiny10 |
# 2025 | 2022 | 2019 | 2016 — see docs/ARCHITECTURE.md).
# (env: WINPODX_WIN_VERSION)
# --win-iso PATH Install from a local Windows ISO at PATH instead of
# downloading it from Microsoft. The ISO is staged into
# the storage dir as `custom.iso` (dockur's bring-your-own
# convention); reflink-copied where the filesystem supports
# it, so it costs no extra space on btrfs/xfs. Saves the
# ~5-8 GB download on every purge/reinstall cycle (#647).
# (env: WINPODX_WIN_ISO)
# --storage-dir PATH On a FRESH install, put the Windows VM disk + ISO at PATH
# (e.g. a roomier partition) instead of
# ~/.local/share/winpodx/storage. The dir is created and
# gets the same prep as the default (chattr +C on btrfs,
# SSD emulation if applicable) (#646). To relocate an
# EXISTING install, use `winpodx setup --migrate-storage
# --migrate-storage-target PATH`. (env: WINPODX_STORAGE_DIR)
# --manual Install winpodx + create the venv only — skip
# 'winpodx setup', 'winpodx pod wait-ready', app
# discovery, and reverse-open. Finish provisioning
# yourself via the first-run prompt on the next
# 'winpodx' invocation. (env: WINPODX_MANUAL=1)
# -h, --help Print this help and exit
###############################################################################
INSTALL_DIR="$HOME/.local/bin/winpodx-app"
VENV_DIR="$INSTALL_DIR/.venv"
VENV_PY="$VENV_DIR/bin/python"
LAUNCHER="$HOME/.local/bin/winpodx-run"
SYMLINK="$HOME/.local/bin/winpodx"
REPO_URL="https://github.com/kernalix7/winpodx.git"
REPO_API="https://api.github.com/repos/kernalix7/winpodx"
# Local-path overrides (env or flag). Flags take precedence over env.
WINPODX_SOURCE="${WINPODX_SOURCE:-}"
WINPODX_IMAGE_TAR="${WINPODX_IMAGE_TAR:-}"
WINPODX_SKIP_DEPS="${WINPODX_SKIP_DEPS:-}"
# v0.2.2.2: explicit ref selection. Empty -> auto-detect latest release tag
# at install time. Set to "main" via --main flag for development builds.
WINPODX_REF="${WINPODX_REF:-}"
# v0.5.x: --win-version flag picks the dockur Windows edition for fresh
# installs (e.g. ltsc10, iot11, tiny11). Empty -> dockur default "11".
# Ignored when an existing winpodx.toml is present (setup skips
# re-configuration for upgrade flows). See #178.
WINPODX_WIN_VERSION="${WINPODX_WIN_VERSION:-}"
WINPODX_WIN_ISO="${WINPODX_WIN_ISO:-}"
WINPODX_STORAGE_DIR="${WINPODX_STORAGE_DIR:-}"
WINPODX_MANUAL="${WINPODX_MANUAL:-}"
# v2: new knobs.
WINPODX_NO_GUI="${WINPODX_NO_GUI:-}"
WINPODX_BACKEND="${WINPODX_BACKEND:-}"
WINPODX_MODE="${WINPODX_MODE:-}"
# Bypass the too-old-podman guard (#271): proceed with podman even when the
# probe sees major < 4, e.g. you upgraded podman out-of-band since.
WINPODX_ALLOW_OLD_PODMAN="${WINPODX_ALLOW_OLD_PODMAN:-}"
# v2: --verbose streams raw container logs during the Windows-boot wait;
# default collapses the ISO download to a clean progress line + hides UEFI noise.
WINPODX_VERBOSE="${WINPODX_VERBOSE:-}"
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
log() { echo -e "${GREEN}[WinPodX]${NC} $*"; }
warn() { echo -e "${YELLOW}[warn]${NC} $*"; }
err() { echo -e "${RED}[error]${NC} $*" >&2; }
usage() {
sed -n '4,93p' "${BASH_SOURCE[0]:-/dev/null}" 2>/dev/null || cat <<'USAGE_EOF'
WinPodX installer — see install.sh header for full usage.
Flags:
--mode r|a|c|n Install mode (Recommended / Automatic / Custom / No)
--backend BACKEND podman | docker | manual
--no-gui Headless install — skip PySide6 in the venv
--verbose, -v Stream raw container logs during the Windows-boot wait
(default: clean ISO-download progress + hidden UEFI noise)
--main Install from git main HEAD (development)
--ref TAG Install a specific tag/branch/commit
--source PATH Copy from local repo instead of git clone
--image-tar PATH Load container image from local tar
--skip-deps Skip distro dependency install
--win-version VER Windows edition for fresh installs
--win-iso PATH Install from a local Windows ISO instead of downloading
--storage-dir PATH Put the VM disk + ISO at PATH (fresh install; roomier partition)
--manual Install binary + venv only — skip provisioning
--allow-old-podman Proceed with podman even if its major version is < 4
(bypass the too-old-podman guard; #271)
-h, --help Print this help and exit
USAGE_EOF
}
# --- Parse flags (must precede any work) ---
while [ $# -gt 0 ]; do
case "$1" in
--main|--dev)
WINPODX_REF="main"
shift
;;
--ref)
WINPODX_REF="${2:-}"
shift 2
;;
--source)
WINPODX_SOURCE="${2:-}"
shift 2
;;
--image-tar)
WINPODX_IMAGE_TAR="${2:-}"
shift 2
;;
--skip-deps)
WINPODX_SKIP_DEPS=1
shift
;;
--win-version)
WINPODX_WIN_VERSION="${2:-}"
if [ -z "$WINPODX_WIN_VERSION" ]; then
err "--win-version requires a value (e.g. ltsc10, iot11, tiny11)"
exit 1
fi
shift 2
;;
--win-iso)
WINPODX_WIN_ISO="${2:-}"
if [ -z "$WINPODX_WIN_ISO" ]; then
err "--win-iso requires a path (e.g. /path/to/Win11.iso)"
exit 1
fi
if [ ! -f "$WINPODX_WIN_ISO" ]; then
err "--win-iso: no such file: $WINPODX_WIN_ISO"
exit 1
fi
# Resolve to an absolute path now — later `cp` may run from a
# different cwd, and the value is logged for the user.
WINPODX_WIN_ISO="$(cd "$(dirname "$WINPODX_WIN_ISO")" && pwd)/$(basename "$WINPODX_WIN_ISO")"
case "$WINPODX_WIN_ISO" in
*.iso|*.ISO) : ;;
*) log "Note: --win-iso path doesn't end in .iso — using it anyway: $WINPODX_WIN_ISO" ;;
esac
shift 2
;;
--storage-dir)
WINPODX_STORAGE_DIR="${2:-}"
if [ -z "$WINPODX_STORAGE_DIR" ]; then
err "--storage-dir requires a path (e.g. /mnt/data/winpodx)"
exit 1
fi
if [ -e "$WINPODX_STORAGE_DIR" ] && [ ! -d "$WINPODX_STORAGE_DIR" ]; then
err "--storage-dir: not a directory: $WINPODX_STORAGE_DIR"
exit 1
fi
# Create it now so we can absolutize + verify it's writable; setup
# re-creates idempotently and applies the btrfs/SSD prep.
if ! mkdir -p "$WINPODX_STORAGE_DIR" 2>/dev/null; then
err "--storage-dir: cannot create $WINPODX_STORAGE_DIR (check the path / permissions)"
exit 1
fi
if [ ! -w "$WINPODX_STORAGE_DIR" ]; then
err "--storage-dir: not writable: $WINPODX_STORAGE_DIR"
exit 1
fi
WINPODX_STORAGE_DIR="$(cd "$WINPODX_STORAGE_DIR" && pwd)"
shift 2
;;
--no-gui)
WINPODX_NO_GUI=1
shift
;;
--verbose|-v)
WINPODX_VERBOSE=1
shift
;;
--backend)
WINPODX_BACKEND="${2:-}"
if [ -z "$WINPODX_BACKEND" ]; then
err "--backend requires a value (podman | docker | manual)"
exit 1
fi
shift 2
;;
--mode)
WINPODX_MODE="${2:-}"
if [ -z "$WINPODX_MODE" ]; then
err "--mode requires a value (r | a | c | n)"
exit 1
fi
shift 2
;;
--manual)
WINPODX_MANUAL=1
shift
;;
--allow-old-podman)
WINPODX_ALLOW_OLD_PODMAN=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
err "Unknown argument: $1"
usage >&2
exit 1
;;
esac
done
# Normalise + validate --backend.
if [ -n "$WINPODX_BACKEND" ]; then
WINPODX_BACKEND="$(echo "$WINPODX_BACKEND" | tr '[:upper:]' '[:lower:]')"
case "$WINPODX_BACKEND" in
podman|docker|manual) ;;
*)
err "--backend must be one of: podman, docker, manual (got '$WINPODX_BACKEND')"
exit 1
;;
esac
fi
# Normalise + validate --mode.
if [ -n "$WINPODX_MODE" ]; then
WINPODX_MODE="$(echo "$WINPODX_MODE" | tr '[:upper:]' '[:lower:]' | cut -c1)"
case "$WINPODX_MODE" in
r|a|c|n) ;;
*)
err "--mode must be one of: r, a, c, n (got '$WINPODX_MODE')"
exit 1
;;
esac
fi
# Validate --source
if [ -n "$WINPODX_SOURCE" ]; then
if [ ! -d "$WINPODX_SOURCE" ]; then
err "--source path does not exist or is not a directory: $WINPODX_SOURCE"
exit 1
fi
if [ ! -f "$WINPODX_SOURCE/pyproject.toml" ] || [ ! -d "$WINPODX_SOURCE/src/winpodx" ]; then
err "--source path does not look like a winpodx repo (missing pyproject.toml or src/winpodx/): $WINPODX_SOURCE"
exit 1
fi
log "Using local source: $WINPODX_SOURCE"
fi
# Validate --image-tar
if [ -n "$WINPODX_IMAGE_TAR" ]; then
if [ ! -f "$WINPODX_IMAGE_TAR" ]; then
err "--image-tar file does not exist: $WINPODX_IMAGE_TAR"
exit 1
fi
log "Using local image tar: $WINPODX_IMAGE_TAR"
fi
if [ -n "$WINPODX_SKIP_DEPS" ]; then
log "Skipping distro dependency install (--skip-deps)"
fi
# --- Detect distro & package manager ---
detect_distro() {
if [ -f /etc/os-release ]; then
. /etc/os-release
echo "$ID"
else
echo "unknown"
fi
}
DISTRO=$(detect_distro)
# Detect host architecture. winpodx ships two dockur image variants:
#
# x86_64 → dockurr/windows (x86_64 Windows guest, native via KVM)
# aarch64 → dockurr/windows-arm (Windows-on-ARM guest, native via KVM)
#
# core/config.py:_default_pod_image picks the matching image on a fresh
# install. install.sh only logs the detected arch here for visibility —
# the image picker fires inside `winpodx setup` later.
ARCH="$(uname -m)"
case "$ARCH" in
x86_64|amd64)
ARCH_LABEL="x86_64"
;;
aarch64|arm64)
ARCH_LABEL="aarch64"
;;
*)
ARCH_LABEL="$ARCH"
;;
esac
# =====================================================================
# Fresh-install detection (governs rollback scope, see trap below).
#
# Rollback is FRESH-INSTALL ONLY: on an upgrade (a prior config OR a
# prior venv exists) a failure must NOT delete the working install. We
# snapshot this BEFORE creating any artifacts this run.
# =====================================================================
PRIOR_CONFIG="$HOME/.config/winpodx/winpodx.toml"
IS_FRESH_INSTALL=1
if [ -f "$PRIOR_CONFIG" ] || [ -e "$VENV_DIR" ]; then
IS_FRESH_INSTALL=0
fi
# =====================================================================
# Rollback (LOCKED scope).
#
# On a FAILED fresh-install run, remove ONLY winpodx's own artifacts
# created THIS run: the venv, the winpodx-run launcher + winpodx
# symlink, the desktop entry + icon, and the .install_in_progress
# marker. Never uninstall system packages; never touch a pre-existing
# ~/.config/winpodx config. On an upgrade, leave everything intact.
# =====================================================================
WINPODX_INSTALL_MARKER="${XDG_CONFIG_HOME:-$HOME/.config}/winpodx/.install_in_progress"
DESKTOP_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
ICON_BASE="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor"
ICON_DIR="$ICON_BASE/scalable/apps"
# Disarmed on success.
ROLLBACK_ARMED=1
cleanup_install_marker() {
rm -f "$WINPODX_INSTALL_MARKER"
}
rollback() {
cleanup_install_marker
if [ "$ROLLBACK_ARMED" -ne 1 ]; then
return 0
fi
if [ "$IS_FRESH_INSTALL" -ne 1 ]; then
warn "Install failed during an upgrade — leaving the existing WinPodX install intact."
warn "Your previous venv, launcher, and config were not touched."
return 0
fi
warn "Rolling back WinPodX install artifacts..."
# venv + cloned/copied source tree (this whole dir is ours, created
# this run on a fresh install).
rm -rf "$INSTALL_DIR" 2>/dev/null || true
rm -f "$LAUNCHER" 2>/dev/null || true
rm -f "$SYMLINK" 2>/dev/null || true
rm -f "$DESKTOP_DIR/winpodx.desktop" 2>/dev/null || true
rm -f "$ICON_DIR/winpodx.svg" 2>/dev/null || true
# Do NOT remove ~/.config/winpodx itself — only our own marker, which
# cleanup_install_marker already handled above.
}
# ERR trap: any failed command (set -e is in effect) rolls back, then
# exits non-zero. INT/TERM abort the rest of install.sh too.
rollback_and_exit_err() {
local rc=$?
rollback
exit "$rc"
}
cleanup_and_exit_int() { rollback; exit 130; }
cleanup_and_exit_term() { rollback; exit 143; }
trap rollback_and_exit_err ERR
trap cleanup_and_exit_int INT
trap cleanup_and_exit_term TERM
# Map generic dependency names to distro-specific package names
pkg_name() {
local dep="$1"
case "$DISTRO" in
opensuse*|sles)
case "$dep" in
python3) echo "python3" ;;
python3-venv) echo "python3" ;;
podman) echo "podman" ;;
docker) echo "docker" ;;
freerdp) echo "freerdp" ;;
kvm) echo "qemu-kvm" ;;
xcb-cursor) echo "libxcb-cursor0" ;;
esac ;;
fedora|rhel|centos|rocky|alma)
case "$dep" in
python3) echo "python3" ;;
python3-venv) echo "python3" ;;
podman) echo "podman" ;;
docker) echo "docker" ;;
freerdp) echo "freerdp" ;;
kvm) echo "qemu-kvm" ;;
xcb-cursor) echo "xcb-util-cursor" ;;
esac ;;
ubuntu|debian|linuxmint|pop)
case "$dep" in
python3) echo "python3" ;;
# Debian/Ubuntu split venv + ensurepip out of python3 into
# python3-venv; without it `python3 -m venv` fails to
# bootstrap pip. This is the one package the mandatory-venv
# step may need to install before the venv can be created.
python3-venv) echo "python3-venv" ;;
podman) echo "podman" ;;
docker) echo "docker.io" ;;
freerdp)
# Debian 13+ (Trixie) and recent Ubuntu (24.10+, 25.04, 25.10)
# only ship freerdp3-x11; the stock freerdp2-x11 package is
# gone. Older systems (Debian <=12, Ubuntu 22.04 stock) only
# ship freerdp2-x11. Ubuntu 24.04 LTS ships both.
# Prefer freerdp3-x11 — the v0.5.1 launcher detects the
# FreeRDP major version at startup and emits the right
# /app: syntax for either version, so freerdp3-x11 is the
# better default when available. Fall back to freerdp2-x11
# only when the user's apt repos don't have a 3 build.
if apt-cache show freerdp3-x11 2>/dev/null | grep -q '^Package:'; then
echo "freerdp3-x11"
elif apt-cache show freerdp2-x11 2>/dev/null | grep -q '^Package:'; then
echo "freerdp2-x11"
else
# Neither in cache — emit freerdp3-x11 so apt produces
# a useful "Unable to locate package" error pointing at
# the recommended package, instead of a stale one.
echo "freerdp3-x11"
fi
;;
kvm)
# Ubuntu 24.04+ (and Mint 22+ / xubuntu 26.04 / Debian 13)
# made qemu-kvm a virtual package with no installation
# candidate; apt errors with
# ``E: Package 'qemu-kvm' has no installation candidate``
# and lists qemu-system-x86 / qemu-system-x86-hwe as the
# real providers (#200, reported by @n-osennij on
# xubuntu 26.04). On aarch64 the real package is
# qemu-system-arm. Probe apt-cache and pick the available
# candidate; fall back to qemu-kvm so apt emits the
# legacy error message on truly ancient repos.
local kvm_first kvm_second
if [ "$ARCH" = "aarch64" ]; then
kvm_first="qemu-system-arm"
kvm_second="qemu-kvm"
else
kvm_first="qemu-system-x86"
kvm_second="qemu-system-x86-hwe"
fi
if apt-cache show "$kvm_first" 2>/dev/null | grep -q '^Package:'; then
echo "$kvm_first"
elif apt-cache show "$kvm_second" 2>/dev/null | grep -q '^Package:'; then
echo "$kvm_second"
elif apt-cache show qemu-kvm 2>/dev/null | grep -q '^Package:'; then
echo "qemu-kvm"
else
echo "$kvm_first"
fi
;;
xcb-cursor) echo "libxcb-cursor0" ;;
esac ;;
arch|manjaro|endeavouros)
case "$dep" in
python3) echo "python" ;;
python3-venv) echo "python" ;;
podman) echo "podman" ;;
docker) echo "docker" ;;
freerdp) echo "freerdp" ;;
kvm) echo "qemu-full" ;;
xcb-cursor) echo "xcb-util-cursor" ;;
esac ;;
*)
echo "$dep" ;;
esac
}
install_pkg() {
local pkg="$1"
local actual
actual=$(pkg_name "$pkg")
log "Installing $actual..."
if command -v zypper >/dev/null 2>&1; then
sudo zypper install -y "$actual"
elif command -v dnf >/dev/null 2>&1; then
sudo dnf install -y "$actual"
elif command -v apt-get >/dev/null 2>&1; then
sudo apt-get install -y "$actual"
elif command -v pacman >/dev/null 2>&1; then
sudo pacman -S --noconfirm "$actual"
else
err "No supported package manager found."
err "Please install '$actual' manually."
return 1
fi
}
# --- Atomic Fedora (Silverblue / Kinoite / Sericea / Bluefin / Bazzite / ...) ---
#
# These distros use rpm-ostree (image-based, immutable root) instead of dnf.
# Layering many packages individually with rpm-ostree triggers a reboot per
# layered batch, so we sidestep the per-dependency loop entirely — winpodx is
# published on the openSUSE Build Service (OBS) as a Fedora RPM whose
# Requires: pulls in freerdp >= 3.0, python3-tomli, and Recommends: podman +
# python3-PySide6, so layering just `winpodx` is one transaction. We try
# `rpm-ostree install --apply-live` first to land the layer in the booted
# deployment without a reboot; if the running deployment can't accept the
# live apply we stage normally and prompt the user to reboot once.
#
# rpm-ostree's RPM install path is wholly separate from the venv flow below,
# so it disarms the rollback trap (its artifacts are an OBS repo file +
# layered RPM, neither of which the venv rollback should touch) and exits.
#
# An EXPLICIT source override (--main / --ref / --source / --image-tar) always
# wins: the OBS RPM only ships tagged releases, so honouring those flags means
# falling through to the git/venv flow even on Atomic (#548 — custom-image
# builders layer winpodx from main like any other Fedora package).
if command -v rpm-ostree >/dev/null 2>&1 \
&& [ -z "$WINPODX_REF" ] && [ -z "$WINPODX_SOURCE" ] && [ -z "$WINPODX_IMAGE_TAR" ]; then
ROLLBACK_ARMED=0
log "Detected rpm-ostree — Atomic Fedora install path."
if [ ! -f /etc/os-release ]; then
err "/etc/os-release missing; can't determine Fedora version for OBS repo selection."
exit 1
fi
. /etc/os-release
obs_ver="$VERSION_ID"
repo_url="https://download.opensuse.org/repositories/home:/Kernalix7/Fedora_${obs_ver}/home:Kernalix7.repo"
log "Probing OBS repo for Fedora ${obs_ver}: $repo_url"
if ! curl -sSfI "$repo_url" >/dev/null 2>&1; then
err "No OBS repo published for Fedora_${obs_ver}."
err "Currently enabled: Fedora 42, 43, 44 at https://build.opensuse.org/project/show/home:Kernalix7"
err "Open an issue at https://github.com/kernalix7/winpodx/issues if you need another Fedora release added."
exit 1
fi
log "Adding OBS repo to /etc/yum.repos.d/..."
sudo curl -sSL "$repo_url" -o /etc/yum.repos.d/home-Kernalix7-winpodx.repo
log "Layering WinPodX via rpm-ostree install --apply-live (one transaction)..."
if sudo rpm-ostree install --apply-live --idempotent winpodx; then
log "WinPodX layered into the booted deployment — no reboot required."
log "Run: winpodx setup"
else
warn "Live apply unavailable on this deployment — staging the layer for next boot."
if sudo rpm-ostree install --idempotent winpodx; then
log "Staged. Reboot the host, then run: winpodx setup"
else
err "rpm-ostree install failed in both live and staged modes. See the rpm-ostree output above for the underlying error."
exit 1
fi
fi
exit 0
elif command -v rpm-ostree >/dev/null 2>&1; then
# rpm-ostree present, but the user asked for a specific source above — note
# the bypass (the OBS RPM only ships tagged releases) and fall through to
# the git/venv install path below.
log "rpm-ostree detected, but --main/--ref/--source/--image-tar was set — installing from source via the venv path."
fi
# =====================================================================
# Pre-sudo system check.
#
# Scan + print a summary BEFORE any sudo / package install so the user
# can see exactly what's present and decide how to proceed.
# =====================================================================
# Helpers used by the scan.
tool_version() {
# Best-effort single-line version string for a tool, or "".
local tool="$1"
case "$tool" in
podman) podman --version 2>/dev/null | head -n1 ;;
docker) docker --version 2>/dev/null | head -n1 ;;
python3) python3 --version 2>&1 | head -n1 ;;
freerdp)
local c
for c in xfreerdp3 xfreerdp wlfreerdp3 wlfreerdp; do
if command -v "$c" >/dev/null 2>&1; then
"$c" --version 2>/dev/null | head -n1 || echo "$c (version unknown)"
return 0
fi
done
;;
esac
}
# Podman major-version gate (#271). dockur/winpodx need rootless
# `group_add: keep-groups` + a modern compose; Ubuntu 22.04 ships
# podman 3.4. Flag podman as too old when major < 4.
PODMAN_PRESENT=false
PODMAN_TOO_OLD=false
PODMAN_MAJOR=0
if command -v podman >/dev/null 2>&1; then
PODMAN_PRESENT=true
# `podman --version` -> "podman version 4.9.3"
PODMAN_MAJOR="$(podman --version 2>/dev/null | sed -n 's/.*version[[:space:]]*\([0-9][0-9]*\).*/\1/p' | head -n1)"
PODMAN_MAJOR="${PODMAN_MAJOR:-0}"
if [ "$PODMAN_MAJOR" -lt 4 ]; then
PODMAN_TOO_OLD=true
fi
fi
# winpodx requires the standalone `podman-compose` (NOT `podman compose`, which
# delegates to docker-compose and breaks our keep-groups extension -- #288).
# It's a separate package the `podman` package doesn't pull in.
PODMAN_COMPOSE_PRESENT=false
command -v podman-compose >/dev/null 2>&1 && PODMAN_COMPOSE_PRESENT=true
DOCKER_PRESENT=false
command -v docker >/dev/null 2>&1 && DOCKER_PRESENT=true
# FreeRDP detection — track native client, Flatpak client, and the Flatpak
# runtime separately so the install policy + Custom mode can choose a source.
# Mirrors core/rdp.py:find_freerdp's accepted native set.
FREERDP_NATIVE_PRESENT=false
for _c in xfreerdp3 xfreerdp sdl-freerdp3 sdl-freerdp wlfreerdp3 wlfreerdp; do
if command -v "$_c" >/dev/null 2>&1; then FREERDP_NATIVE_PRESENT=true; break; fi
done
FLATPAK_PRESENT=false
command -v flatpak >/dev/null 2>&1 && FLATPAK_PRESENT=true
FREERDP_FLATPAK_PRESENT=false
if [ "$FLATPAK_PRESENT" = true ] \
&& flatpak list --app --columns=application 2>/dev/null | grep -qx 'com.freerdp.FreeRDP'; then
FREERDP_FLATPAK_PRESENT=true
fi
# Any client (native or Flatpak) satisfies the requirement, so we never pull a
# redundant native package when a client is already present (#269). winpodx's
# launcher prefers the FLATPAK client when both are present (core/rdp.py
# auto order is flatpak-first, #401) — so the reported "kind" mirrors that:
# flatpak wins the label when both exist, so it matches the client the
# launcher will actually use.
FREERDP_PRESENT=false
FREERDP_KIND=""
if [ "$FREERDP_FLATPAK_PRESENT" = true ]; then
FREERDP_PRESENT=true
FREERDP_KIND="flatpak (com.freerdp.FreeRDP)"
elif [ "$FREERDP_NATIVE_PRESENT" = true ]; then
FREERDP_PRESENT=true
FREERDP_KIND="native"
fi
PYTHON3_PRESENT=false
command -v python3 >/dev/null 2>&1 && PYTHON3_PRESENT=true
KVM_PRESENT=false
[ -e /dev/kvm ] && KVM_PRESENT=true
# Probe whether `python3 -m venv` can actually bootstrap (Debian/Ubuntu
# split python3-venv / ensurepip out).
VENV_PROBE_OK=false
if [ "$PYTHON3_PRESENT" = true ] && python3 -c "import venv, ensurepip" >/dev/null 2>&1; then
VENV_PROBE_OK=true
fi
yesno() { if [ "$1" = true ]; then echo "yes"; else echo "no"; fi; }
print_system_check() {
local osname="$DISTRO"
if [ -f /etc/os-release ]; then
# shellcheck disable=SC1091
. /etc/os-release
osname="${PRETTY_NAME:-$DISTRO}"
fi
echo ""
echo "================ WinPodX system check ================"
echo " Distro: $osname"
echo " Architecture: $ARCH_LABEL"
echo " python3: $(yesno "$PYTHON3_PRESENT") $(tool_version python3)"
echo " python venv: $(yesno "$VENV_PROBE_OK") (python3 -m venv works)"
if [ "$PODMAN_PRESENT" = true ]; then
if [ "$PODMAN_TOO_OLD" = true ]; then
echo " podman: yes $(tool_version podman) [TOO OLD — major $PODMAN_MAJOR < 4]"
else
echo " podman: yes $(tool_version podman)"
fi
else
echo " podman: no"
fi
echo " docker: $(yesno "$DOCKER_PRESENT") $(tool_version docker)"
echo " freerdp: $(yesno "$FREERDP_PRESENT") ${FREERDP_KIND:+$FREERDP_KIND }$(tool_version freerdp)"
echo " /dev/kvm: $(yesno "$KVM_PRESENT")"
echo "======================================================"
if [ "$PODMAN_TOO_OLD" = true ]; then
echo ""
warn "podman $PODMAN_MAJOR.x is too old for WinPodX (need >= 4 for rootless"
warn "'group_add: keep-groups' + modern compose; Ubuntu 22.04 ships 3.4 — #271)."
warn "Either:"
warn " - upgrade podman (Kubic / devel:kubic:libcontainers repo:"
warn " https://software.opensuse.org/download/package?package=podman&project=devel%3Akubic%3Alibcontainers%3Aunstable ), OR"
warn " - use the Docker backend (--backend docker, or pick Docker in Custom mode)."
fi
echo ""
}
print_system_check
# =====================================================================
# Mode prompt.
#
# Resolve INSTALL_MODE (r/a/c/n). Precedence:
# 1. --mode / WINPODX_MODE if set.
# 2. interactive TTY -> prompt.
# 3. non-interactive -> 'r' (recommended), preserving old behaviour.
# =====================================================================
INSTALL_MODE="$WINPODX_MODE"
# Interactive if we can reach a real terminal for INPUT — either stdin is a
# TTY, or (the `curl ... | bash` case, where stdin is the script pipe) the
# controlling terminal /dev/tty is openable. Prompts read from $TTY_DEV so the
# mode menu works even via curl|bash. A run with no controlling terminal
# (CI / cron / `</dev/null`) stays non-interactive and defaults to Recommended.
INTERACTIVE=false
TTY_DEV=/dev/stdin
if [ -t 0 ]; then
INTERACTIVE=true
TTY_DEV=/dev/stdin
elif { true </dev/tty; } 2>/dev/null; then
INTERACTIVE=true
TTY_DEV=/dev/tty
fi
if [ -z "$INSTALL_MODE" ]; then
if [ "$IS_FRESH_INSTALL" != "1" ]; then
# Upgrade / re-run: a winpodx config (and backend) already exists, so
# the R/A/C/N mode prompt is pointless — there's nothing to choose, we
# reuse the existing setup and run the migrate path. Resolve to
# Automatic (reuse what's present, install only what's strictly
# missing) without prompting.
INSTALL_MODE="a"
log "Existing WinPodX install detected — upgrading; reusing current config (skipping install-mode prompt)."
elif [ "$INTERACTIVE" = true ]; then
cat <<'MODE_EOF'
Install mode?
[R]ecommended WinPodX's recommended stack (Podman backend + deps); installs missing system packages via the distro package manager (sudo).
[A]utomatic Reuse what's already installed; only install what's strictly missing; pick the backend from what's present (prefer an already-working docker/podman). Minimal sudo.
[C]ustom Choose backend (podman/docker) and whether to include the GUI, then install accordingly.
[N]o Cancel without changing anything.
MODE_EOF
echo -n "Choice [R/a/c/n]: "
read -r mode_answer < "$TTY_DEV"
case "$(echo "${mode_answer:-r}" | tr '[:upper:]' '[:lower:]' | cut -c1)" in
a) INSTALL_MODE="a" ;;
c) INSTALL_MODE="c" ;;
n) INSTALL_MODE="n" ;;
*) INSTALL_MODE="r" ;;
esac
else
INSTALL_MODE="r"
log "Non-interactive run with no --mode — defaulting to Recommended (r)."
fi
fi
# Mode N: clean exit, no changes. Rollback is a no-op on a fresh pre-install run.
if [ "$INSTALL_MODE" = "n" ]; then
log "Cancelled, no changes made."
ROLLBACK_ARMED=0
cleanup_install_marker
trap - EXIT ERR INT TERM
exit 0
fi
# --- Custom mode: interactively pick the source of each major dependency ---
# Components with a real source choice: the container backend (podman /
# docker), the FreeRDP client (native package / Flatpak), and the GUI
# (PySide6 yes/no). Everything else is a plain distro package with no
# meaningful alternative. Each prompt has a sensible default so just hitting
# Enter reproduces the Recommended stack.
if [ "$INSTALL_MODE" = "c" ]; then
if [ -z "$WINPODX_BACKEND" ]; then
if [ "$INTERACTIVE" = true ]; then
echo -n "Container backend? [podman/docker] (default podman): "
read -r be_answer < "$TTY_DEV"
case "$(echo "${be_answer:-podman}" | tr '[:upper:]' '[:lower:]')" in
docker) WINPODX_BACKEND="docker" ;;
*) WINPODX_BACKEND="podman" ;;
esac
else
WINPODX_BACKEND="podman"
fi
fi
if [ -z "${WINPODX_FREERDP_SOURCE:-}" ] && [ "$INTERACTIVE" = true ]; then
echo "FreeRDP client source?"
echo " [A]uto prefer the Flatpak when present, else native (recommended)"
echo " [N]ative distro freerdp3 package"
echo " [F]latpak com.freerdp.FreeRDP via Flatpak (better on distros with a broken native freerdp3-x11)"
echo -n "Choice [A/n/f]: "
read -r fr_answer < "$TTY_DEV"
case "$(echo "${fr_answer:-a}" | tr '[:upper:]' '[:lower:]' | cut -c1)" in
n) WINPODX_FREERDP_SOURCE="native" ;;
f) WINPODX_FREERDP_SOURCE="flatpak" ;;
*) WINPODX_FREERDP_SOURCE="auto" ;;
esac
fi
if [ -z "$WINPODX_NO_GUI" ] && [ "$INTERACTIVE" = true ]; then
echo -n "Install the GUI (PySide6)? [Y/n]: "
read -r gui_answer < "$TTY_DEV"
if [[ "$gui_answer" =~ ^[Nn] ]]; then
WINPODX_NO_GUI=1
fi
fi
fi
# --- Automatic mode: pick a usable, already-present backend ---
# Walk winpodx's RECOMMENDED backend order (podman first, then docker --
# podman is the project default per CLAUDE.md; libvirt was dropped in 0.6.0)
# and select the first one that's both present AND usable. So when several runtimes are
# installed, the recommended one wins; we only move down the list when the
# higher-priority runtime is absent or unusable (e.g. podman < 4 on Ubuntu
# 22.04, #271). Fall back to Recommended behaviour (podman + install missing
# deps) when nothing usable is present.
#
# Single source of truth for this priority + the podman major-version gate
# is src/winpodx/backend/select.py:choose_backend() (Python). This bash mirror
# exists because install.sh runs system-check BEFORE Python is installed and
# can't import the Python helper; tests pin the bash + Python copies to the
# same order + version gate so they cannot drift. See docs/design/ROADMAP-
# 0.6.0.md item E.
if [ "$INSTALL_MODE" = "a" ] && [ -z "$WINPODX_BACKEND" ]; then
for candidate in podman docker; do
case "$candidate" in
podman)
[ "$PODMAN_PRESENT" = true ] && [ "$PODMAN_TOO_OLD" = false ] || continue
WINPODX_BACKEND="podman"
log "Automatic: podman $PODMAN_MAJOR.x present + usable — selecting podman (recommended)."
;;
docker)
[ "$DOCKER_PRESENT" = true ] || continue
WINPODX_BACKEND="docker"
log "Automatic: podman absent/too-old, docker present — selecting docker."
;;
esac
break
done
if [ -z "$WINPODX_BACKEND" ]; then
WINPODX_BACKEND="podman"
warn "Automatic: no usable runtime present — falling back to Recommended (podman + install missing deps)."
fi
fi
# --- Recommended mode (default): podman unless --backend given ---
if [ "$INSTALL_MODE" = "r" ] && [ -z "$WINPODX_BACKEND" ]; then
WINPODX_BACKEND="podman"
fi
# Backend 'manual' implies --manual (skip the provisioning chain).
if [ "$WINPODX_BACKEND" = "manual" ]; then
WINPODX_MANUAL=1
fi
# --- Too-old-podman guard (#271, ask 3: graceful handling) ---
# Automatic mode (above) already walks past an unusable podman to docker,
# but Recommended mode and an explicit `--backend podman` do not, so
# a host with podman < 4 (Ubuntu 22.04 ships 3.4) would proceed and then fail
# at provisioning -- AFTER we'd installed packages. Refuse to blindly
# continue: offer a usable alternative when one is present, otherwise exit
# cleanly WITHOUT modifying the system (no package install has run yet here).
# WINPODX_ALLOW_OLD_PODMAN=1 / --allow-old-podman overrides (e.g. podman was
# upgraded out-of-band since the probe).
if [ "$WINPODX_BACKEND" = "podman" ] && [ "$PODMAN_TOO_OLD" = true ] && [ "$WINPODX_ALLOW_OLD_PODMAN" != "1" ]; then
warn "podman $PODMAN_MAJOR.x is too old for WinPodX (need >= 4; Ubuntu 22.04 ships 3.4 -- #271)."
if [ "$INTERACTIVE" = true ]; then
if [ "$DOCKER_PRESENT" = true ]; then
echo "docker is installed and usable."
echo " [d] switch to the docker backend"
echo " [c] continue with podman anyway (will likely fail)"
echo " [a] abort without changing anything (default)"
printf 'Choice [d/c/a]: '
read -r _pm_choice < "$TTY_DEV"
case "$(echo "${_pm_choice:-a}" | tr '[:upper:]' '[:lower:]')" in
d|docker)
WINPODX_BACKEND="docker"; log "Switched backend to docker."
;;
c|continue)
warn "Continuing with podman $PODMAN_MAJOR.x at your own risk."
;;
*)
err "Aborted: upgrade podman to >= 4 or re-run with --backend docker. No changes were made."
exit 1
;;
esac
else
echo "No alternative backend (docker) is installed."
printf 'Continue with podman %s.x anyway? It will likely fail. [y/N]: ' "$PODMAN_MAJOR"
read -r _pm_choice < "$TTY_DEV"
case "$(echo "${_pm_choice:-n}" | tr '[:upper:]' '[:lower:]')" in
y|yes)
warn "Continuing with podman $PODMAN_MAJOR.x at your own risk."
;;
*)
err "Aborted: upgrade podman to >= 4 (e.g. the Kubic repo) or install docker, then re-run. No changes were made."
exit 1
;;
esac
fi
else
# Non-interactive: do NOT silently switch the backend or run a known-
# failing podman install. Exit cleanly with guidance.
if [ "$DOCKER_PRESENT" = true ]; then
warn "docker is installed -- re-run with --backend docker."
fi
warn "Or upgrade podman to >= 4, or pass --allow-old-podman / WINPODX_ALLOW_OLD_PODMAN=1 to force."
err "Non-interactive install aborted before modifying the system: backend=podman but podman $PODMAN_MAJOR.x is too old (#271)."
exit 1
fi
fi
log "Detected distro: $DISTRO"
log "Detected arch: $ARCH_LABEL"
log "Install mode: $INSTALL_MODE | backend: ${WINPODX_BACKEND:-podman} | gui: $([ -n "$WINPODX_NO_GUI" ] && echo no || echo yes)"
# Mark the install as in-progress so child winpodx CLI invocations
# (winpodx setup, pod wait-ready, migrate, app refresh, host-open
# refresh, ...) skip the tray auto-spawn AND the tray's UNRESPONSIVE
# auto-recovery transition. Without this, install.sh's wait windows --
# where the guest is genuinely booting and RDP legitimately isn't
# reachable -- would have the tray fire a "Pod stopped responding"
# notification + try to Restart-Service TermService against a guest
# that's still running first-boot Sysprep. Marker is removed on every
# exit path via the traps above.
mkdir -p "$(dirname "$WINPODX_INSTALL_MARKER")"
echo "$$" > "$WINPODX_INSTALL_MARKER"
chmod 600 "$WINPODX_INSTALL_MARKER" 2>/dev/null || true
# =====================================================================
# Which required system deps are missing?
#
# Mode shapes what we install:
# R -> install all genuinely-missing required deps via pkg mgr.
# A -> install only genuinely-missing required deps (same set; A
# differs only in backend selection, which is already done).
# C -> install deps appropriate to the chosen backend.
# The chosen backend decides whether podman/docker is required.
# =====================================================================
log "Checking dependencies..."
MISSING=()
# Backend runtime requirement.
case "${WINPODX_BACKEND:-podman}" in
podman)
if [ "$PODMAN_PRESENT" = false ] || [ "$PODMAN_TOO_OLD" = true ]; then
# In A mode we only install genuinely-missing deps; a too-old
# podman is "present", so don't try to replace it via pkg mgr
# (the distro repo would reinstall the same old version). Warn
# instead. In R/C we add podman to MISSING only when absent.
if [ "$PODMAN_PRESENT" = false ]; then
MISSING+=("podman")
else
warn "podman is present but too old (major $PODMAN_MAJOR); see the note above. Continuing — pod start may fail until you upgrade podman or switch to --backend docker."
fi
fi
# podman-compose is required but ships separately from podman; without
# it pod creation later fails with "compose command not found" (#503,
# #580). Install it alongside podman.
[ "$PODMAN_COMPOSE_PRESENT" = false ] && MISSING+=("podman-compose")
;;
docker)
[ "$DOCKER_PRESENT" = false ] && MISSING+=("docker")
;;
esac
# FreeRDP is required for every backend (the launcher shells out to it).
# Source resolution (auto | native | flatpak). Custom mode may set
# WINPODX_FREERDP_SOURCE; default auto. winpodx's launcher prefers the FLATPAK
# client when present (flatpak-first auto order, #401). install.sh still
# installs the lightweight NATIVE package when NO client is present at all --
# pulling the whole Flatpak runtime during install is heavy, and the launcher
# uses whatever is there. An already-present Flatpak is reused (never install a
# redundant client). Only an explicit `--freerdp-source flatpak` (Custom)
# installs the Flatpak. The resolved value -> `winpodx setup --freerdp-source`.
WINPODX_FREERDP_SOURCE="${WINPODX_FREERDP_SOURCE:-auto}"
INSTALL_FREERDP_FLATPAK=false
case "$WINPODX_FREERDP_SOURCE" in
flatpak)
# Explicit Flatpak choice (e.g. native freerdp3-x11 is broken, #393).
if [ "$FREERDP_FLATPAK_PRESENT" = false ]; then
INSTALL_FREERDP_FLATPAK=true
[ "$FLATPAK_PRESENT" = false ] && MISSING+=("flatpak")
fi
;;
*) # auto: the launcher prefers an existing Flatpak (flatpak-first), but
# install the lightweight native package only when NO client (native
# or Flatpak) is present — an existing Flatpak is reused, never a
# redundant install.
[ "$FREERDP_PRESENT" = false ] && MISSING+=("freerdp")
;;
esac
# python3 is mandatory (venv host interpreter).
[ "$PYTHON3_PRESENT" = false ] && MISSING+=("python3")
# The Qt6 GUI (PySide6) needs the xcb-cursor platform-plugin runtime library
# (libxcb-cursor.so.0). Qt 6.5+ refuses to start its xcb platform plugin
# without it -- "could not load the Qt platform plugin 'xcb'" -- and it isn't
# pulled in transitively on a minimal desktop (e.g. fresh Linux Mint 22, #712).
# Only add it when the GUI is enabled and the lib isn't already present.
#
# Detection probes the real .so on disk, NOT `ldconfig -p`: on openSUSE the
# library exists at /usr/lib64/libxcb-cursor.so.0 but is absent from the ld.so
# cache, so the ldconfig probe false-negatived and re-prompted every run even
# with libxcb-cursor0 installed (#712 follow-up). Check the standard lib dirs
# (incl. Debian/Ubuntu multiarch) directly, then fall back to ldconfig for any
# non-standard prefix.
_has_libxcb_cursor() {
local d
for d in /usr/lib64 /usr/lib /lib64 /lib \
/usr/lib/x86_64-linux-gnu /usr/lib/aarch64-linux-gnu; do
[ -e "$d/libxcb-cursor.so.0" ] && return 0
done
ldconfig -p 2>/dev/null | grep -q 'libxcb-cursor\.so\.0'
}
if [ -z "$WINPODX_NO_GUI" ] && ! _has_libxcb_cursor; then
MISSING+=("xcb-cursor")
fi
if [ "$KVM_PRESENT" = false ]; then
# Pre-install hint. A surprising fraction of user bug reports start
# here -- the package install loop below will run successfully on
# most distros because qemu / qemu-kvm is already present, and then
# the container start later silently fails because hardware virt
# is off in BIOS. Print the BIOS / module / group check now so the
# user can stop, fix the actual cause, and re-run.
warn "/dev/kvm not found -- KVM hardware virtualization is required."
warn ""
warn "Before continuing, please verify:"
warn " 1. Intel VT-x / AMD-V is enabled in your BIOS / UEFI."
warn " (Reboot -> firmware setup -> 'Intel Virtualization Technology' /"
warn " 'SVM Mode' / 'VT-x'. The setting is OFF by default on many laptops.)"
warn " 2. The kvm kernel module is loaded:"
if command -v lsmod >/dev/null 2>&1; then
modules=$(lsmod 2>/dev/null | grep -E '^(kvm|kvm_intel|kvm_amd)\b' | awk '{print $1}' | tr '\n' ' ')
warn " Currently loaded: ${modules:-none}"
warn " Load it with: sudo modprobe kvm_intel (or kvm_amd on AMD)"
fi
warn " 3. Your user is in the 'kvm' group: id -nG | tr ' ' '\\n' | grep kvm"
warn ""
warn "Installing the qemu package alone won't fix BIOS / module / group issues."
warn "Most 'install ran fine but Windows never boots' bug reports trace back here."
warn ""
MISSING+=("kvm")
fi
# --- Plan summary -----------------------------------------------------------
# Now that the mode + every dependency source is resolved, show exactly what
# this run will do BEFORE touching the system (installing packages, sudo,
# provisioning Windows). Every major component is listed evenly with its
# detected state + the action this run will take. Pure logging — non-blocking.
#
# Per-component action helper: "$present" true => use existing, else (if the
# component name is in MISSING) install, else a component-specific note.
_in_missing() { local x; for x in "${MISSING[@]}"; do [ "$x" = "$1" ] && return 0; done; return 1; }
_plan_action() { # <present-bool> <missing-key>
if [ "$1" = true ]; then echo "use existing"; elif _in_missing "$2"; then echo "install"; else echo "—"; fi
}
_resolved_backend="${WINPODX_BACKEND:-podman}"
case "$_resolved_backend" in
podman) _backend_present="$PODMAN_PRESENT" ;;
docker) _backend_present="$DOCKER_PRESENT" ;;
*) _backend_present=false ;;
esac
echo ""
log "==================== install plan ===================="
log " Mode: $INSTALL_MODE (r=recommended / a=automatic / c=custom)"
log " python3: $(yesno "$PYTHON3_PRESENT") $(tool_version python3) [host interpreter + private venv]"
log " venv: $(yesno "$VENV_PROBE_OK") [python3 -m venv works]"
log " Backend: $_resolved_backend ($(_plan_action "$_backend_present" "$_resolved_backend"))"
if [ "$FREERDP_PRESENT" = true ]; then
log " FreeRDP: use existing — $FREERDP_KIND"
elif [ "$INSTALL_FREERDP_FLATPAK" = true ]; then
log " FreeRDP: install Flatpak (com.freerdp.FreeRDP)"
else
log " FreeRDP: install native package"
fi
log " KVM: $(yesno "$KVM_PRESENT") /dev/kvm [host virtualization — required, not installed]"
log " GUI: $([ -n "$WINPODX_NO_GUI" ] && echo 'no (--no-gui)' || echo 'yes (PySide6, into venv)')"
if [ "${#MISSING[@]}" -gt 0 ]; then
log " Will install: ${MISSING[*]} (via distro package manager, sudo)"
else
log " Will install: nothing — all required system packages already present"
fi
if [ "${WINPODX_MANUAL:-0}" = "1" ] || [ "$WINPODX_BACKEND" = "manual" ]; then
log " Windows VM: NOT provisioned (manual mode); run 'winpodx setup' later to finish"
else
log " Windows VM: setup -> first-boot (~7.5GB ISO on a fresh install)"
log " -> apply-fixes -> discovery -> reverse-open"
fi
log "======================================================"
echo ""
if [ ${#MISSING[@]} -gt 0 ]; then
if [ -n "$WINPODX_SKIP_DEPS" ]; then
err "--skip-deps is set but required tools are missing: ${MISSING[*]}"
err "Install them manually and re-run, or drop --skip-deps."
exit 1
fi
log "Missing: ${MISSING[*]}"
echo ""
echo " The following will be installed via $(command -v zypper || command -v dnf || command -v apt-get || command -v pacman):"
for dep in "${MISSING[@]}"; do
echo " - $(pkg_name "$dep")"
done
echo ""
if [ "$INTERACTIVE" = true ]; then
echo -n " Proceed with installation? (Y/n): "
read -r answer < "$TTY_DEV"
if [[ "$answer" =~ ^[Nn] ]]; then
err "Aborted. Install dependencies manually and try again."
exit 1
fi
else
log " Non-interactive — proceeding with package install."
fi
INSTALL_FAIL=0
for dep in "${MISSING[@]}"; do
if ! install_pkg "$dep"; then
warn "Failed to install: $(pkg_name "$dep")"
INSTALL_FAIL=$((INSTALL_FAIL + 1))
fi
done
if [ "$INSTALL_FAIL" -gt 0 ]; then
err "$INSTALL_FAIL package(s) failed to install. Fix manually and re-run."
exit 1
fi
log "All dependencies installed successfully"
else
log "All dependencies OK"
fi
# Install the Flatpak FreeRDP when that's the resolved source (auto with a
# flatpak runtime but no client yet, or an explicit --freerdp-source flatpak).
# Best-effort: if the Flatpak install fails (no flathub remote, offline, …)
# fall back to the native package so the user still ends up with a client.
if [ "$INSTALL_FREERDP_FLATPAK" = true ]; then
log "Installing FreeRDP via Flatpak (com.freerdp.FreeRDP)..."
flatpak remote-add --if-not-exists --user flathub \
https://dl.flathub.org/repo/flathub.flatpakrepo >/dev/null 2>&1 || true
if flatpak install -y --user flathub com.freerdp.FreeRDP >/dev/null 2>&1 \
|| flatpak install -y flathub com.freerdp.FreeRDP >/dev/null 2>&1; then
log " Flatpak FreeRDP installed."
FREERDP_PRESENT=true
WINPODX_FREERDP_SOURCE="flatpak"
else
warn " Flatpak FreeRDP install failed — falling back to the native package."
WINPODX_FREERDP_SOURCE="auto"
if [ "$FREERDP_NATIVE_PRESENT" = false ]; then
if install_pkg freerdp; then
FREERDP_PRESENT=true
else
warn " Native FreeRDP install also failed — install a FreeRDP 3 client manually."
fi
fi
fi
fi
# Re-verify /dev/kvm after the install loop. Installing the qemu /
# qemu-kvm package alone does NOT enable hardware virtualisation if
# the CPU extension is off in BIOS, the kvm kernel module isn't
# loaded, or the user isn't in the `kvm` group -- @pnogaret2019-code
# hit this on Linux Mint LMDE 7 (#220) where apt happily said
# "qemu-system-x86 already up to date" while /dev/kvm stayed absent,
# and install.sh printed "All dependencies installed successfully"
# anyway. Without this guard the container starts but the VM never
# boots, and the user sees a silent stall instead of the actionable
# diagnostic below.
#
# #541: /dev/kvm is often absent simply because the kvm kernel module
# isn't loaded -- even though the CPU fully supports virtualization
# (vmx/svm flags present; e.g. confirmed by VirtualBox working on the
# same box). Try to load it before failing -- many "virtualization
# unsupported" reports just needed a modprobe.
if [ ! -e /dev/kvm ] && grep -Eq '(vmx|svm)' /proc/cpuinfo 2>/dev/null; then
if grep -q 'vmx' /proc/cpuinfo 2>/dev/null; then KVM_MOD=kvm_intel; else KVM_MOD=kvm_amd; fi
log "/dev/kvm missing but the CPU reports virtualization (vmx/svm) -- loading $KVM_MOD..."
sudo modprobe "$KVM_MOD" 2>/dev/null || true
if [ -e /dev/kvm ]; then
KVM_PRESENT=true
log "/dev/kvm is now present -- hardware virtualization enabled."
# Persist so it auto-loads on the next boot (don't make the user redo this).
if [ -d /etc/modules-load.d ]; then
printf '%s\n' "$KVM_MOD" | sudo tee /etc/modules-load.d/winpodx-kvm.conf >/dev/null 2>&1 || true
fi
fi
fi
if [ ! -e /dev/kvm ]; then
err "/dev/kvm still missing after package install."
err ""
err "Hardware virtualisation is required for WinPodX. Likely causes:"
err " 1. Intel VT-x / AMD-V disabled in BIOS / UEFI."
err " -> Reboot, enter setup, look for 'Intel Virtualization Technology'"
err " / 'SVM Mode' / 'VT-x' and enable it."
err " 2. kvm kernel module not loaded:"
if command -v lscpu >/dev/null 2>&1; then
vmx=$(lscpu 2>/dev/null | grep -i 'virtualization\|vmx\|svm' | head -1)
err " lscpu: ${vmx:-no virtualization line found}"
fi
if command -v lsmod >/dev/null 2>&1; then
modules=$(lsmod 2>/dev/null | grep -E '^(kvm|kvm_intel|kvm_amd)\b' | awk '{print $1}' | tr '\n' ' ')
err " Loaded kvm modules: ${modules:-none}"
err " -> 'sudo modprobe kvm_intel' (Intel) or 'sudo modprobe kvm_amd' (AMD)"
fi
err " 3. Your user is not in the 'kvm' group:"
if command -v id >/dev/null 2>&1; then
if id -nG "$USER" 2>/dev/null | tr ' ' '\n' | grep -qx kvm; then
err " id: '$USER' is in the kvm group (this one is fine)."
else
err " id: '$USER' is NOT in the kvm group."
err " -> 'sudo usermod -aG kvm $USER' then log out + back in."
fi
fi
err ""
err "Fix one of the above and re-run install.sh."
exit 1
fi
# --- Check Python version (host interpreter that builds the venv) ---
PY_VERSION=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
PY_MAJOR=$(echo "$PY_VERSION" | cut -d. -f1)
PY_MINOR=$(echo "$PY_VERSION" | cut -d. -f2)
if [ "$PY_MAJOR" -lt 3 ] || { [ "$PY_MAJOR" -eq 3 ] && [ "$PY_MINOR" -lt 9 ]; }; then
err "Python 3.9+ required (found $PY_VERSION)"
exit 1
fi
log "Python $PY_VERSION OK"
# --- Clone, update, or copy winpodx source ---
mkdir -p "$(dirname "$INSTALL_DIR")"
copy_from_local() {
local src="$1"
if [ -d "$INSTALL_DIR" ]; then
rm -rf "$INSTALL_DIR"
fi
mkdir -p "$INSTALL_DIR"
for item in src data config scripts install.sh uninstall.sh pyproject.toml README.md LICENSE; do
if [ -e "$src/$item" ]; then
cp -r "$src/$item" "$INSTALL_DIR/"
fi
done
}
# Resolve the install ref. Default (empty WINPODX_REF) -> latest release.
# `--main` / WINPODX_REF=main bypasses the API call so an unreachable
# api.github.com still lets dev installs proceed.
resolve_ref() {
if [ -n "$WINPODX_REF" ]; then
echo "$WINPODX_REF"
return
fi
if ! command -v curl >/dev/null 2>&1; then
echo "main"
return
fi
local latest
latest=$(curl -fsSL "$REPO_API/releases/latest" 2>/dev/null \
| sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \
| head -n1)
if [ -n "$latest" ]; then
echo "$latest"
else
echo "main"
fi
}
if [ -n "$WINPODX_SOURCE" ]; then
# --source wins over every other path; no git at all.
log "Copying WinPodX from --source: $WINPODX_SOURCE"
copy_from_local "$WINPODX_SOURCE"
else
INSTALL_REF="$(resolve_ref)"
if [ "$INSTALL_REF" = "main" ]; then
log "Installing from git main (development)"
else
log "Installing release: $INSTALL_REF (use --main for development build)"
fi
if [ -d "$INSTALL_DIR/.git" ]; then
log "Updating existing installation to $INSTALL_REF..."
git -C "$INSTALL_DIR" fetch --quiet --tags --prune origin
# Check out the freshly-fetched ref. Prefer origin/<ref> so a BRANCH
# advances to its latest remote tip — `git fetch` updates origin/<ref>
# but NOT the stale local branch, so a plain `checkout <branch>` would
# pin the install to whatever commit was first cloned (#616: every
# re-run reinstalled the same old commit). Fall back to <ref> for tags
# / commits, which aren't origin/-prefixed.
git -C "$INSTALL_DIR" checkout --quiet --detach "origin/$INSTALL_REF" 2>/dev/null \
|| git -C "$INSTALL_DIR" checkout --quiet --detach "$INSTALL_REF" 2>/dev/null \
|| git -C "$INSTALL_DIR" checkout --quiet "$INSTALL_REF"
else
# If running from repo, copy only needed files (skip .venv, .git, etc.).
# When piped via `curl ... | bash`, bash reads from stdin and
# BASH_SOURCE[0] is unset — `set -u` would abort here without the
# default expansion. Fall through to git-clone when there's no local tree.
_src="${BASH_SOURCE[0]:-}"
if [ -n "$_src" ]; then
SCRIPT_DIR="$(cd "$(dirname "$_src")" && pwd)"
else
SCRIPT_DIR=""
fi
if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/src/winpodx/__init__.py" ]; then
log "Installing from local repository..."
copy_from_local "$SCRIPT_DIR"
else
if ! command -v git >/dev/null 2>&1; then
# git isn't preinstalled on some fresh systems (e.g. Linux Mint);
# auto-install it like every other dependency so the one-liner
# is self-contained (#705). Fall back to a clear error if the
# package manager can't provide it.
log "git is required to fetch WinPodX but isn't installed; installing it..."
install_pkg git || true
if ! command -v git >/dev/null 2>&1; then
err "git is required for remote install but could not be installed automatically."
err "Install git manually (e.g. 'sudo apt-get install git') and re-run, or run from a cloned repository."
exit 1
fi
fi
log "Cloning from GitHub..."
if [ -d "$INSTALL_DIR" ]; then
rm -rf "$INSTALL_DIR"
fi
git clone --quiet "$REPO_URL" "$INSTALL_DIR"
git -C "$INSTALL_DIR" fetch --quiet --tags --prune origin
# origin/<ref> first so a branch lands on its latest tip; fall
# back to <ref> for tags / commits (see the update path above).
git -C "$INSTALL_DIR" checkout --quiet --detach "origin/$INSTALL_REF" 2>/dev/null \
|| git -C "$INSTALL_DIR" checkout --quiet --detach "$INSTALL_REF" 2>/dev/null \
|| git -C "$INSTALL_DIR" checkout --quiet "$INSTALL_REF"
fi
fi
fi
# =====================================================================
# Mandatory venv (all modes).
#
# Python ALWAYS runs from a private venv. Create it; if creation fails
# because python3-venv / ensurepip is missing, install that one package
# via the distro pkg mgr (sudo) in R/A/C, then retry; if still failing,
# error out (the ERR trap rolls back on a fresh install).
# =====================================================================
create_venv() {
rm -rf "$VENV_DIR"
python3 -m venv "$VENV_DIR"
}
log "Creating private virtualenv at $VENV_DIR ..."
if ! create_venv 2>/dev/null; then
if [ -n "$WINPODX_SKIP_DEPS" ]; then
err "python3 -m venv failed and --skip-deps is set."
err "Install your distro's python3-venv / ensurepip package and re-run."
exit 1
fi
warn "venv creation failed — likely a missing python3-venv / ensurepip. Installing it..."
install_pkg "python3-venv" || true
if ! create_venv; then
err "venv creation still failing after installing $(pkg_name python3-venv)."
err "Install your distro's python3-venv / ensurepip package manually and re-run."
exit 1
fi
fi
# Upgrade pip/setuptools/wheel in the venv (quiet; non-fatal cosmetics).
"$VENV_PY" -m pip install --quiet --upgrade pip setuptools wheel || \
warn "pip self-upgrade failed; continuing with the bundled pip."
# Install winpodx itself from the in-place source tree. This resolves
# winpodx's own declared runtime deps (tomli on 3.9/3.10 via the
# python_version marker) from pyproject. We then add the reverse-open
# icon deps (cairosvg + pyxdg) and, unless --no-gui, PySide6 — pinned to
# the same ranges pyproject declares so we don't invent versions.
log "Installing WinPodX into the venv (pip install '$INSTALL_DIR')..."
if [ -n "$WINPODX_NO_GUI" ]; then
# Headless: winpodx core + reverse-open icon quality, no PySide6.
"$VENV_PY" -m pip install --quiet "${INSTALL_DIR}[reverse-open]"
log "Headless install (--no-gui): PySide6 skipped."
else
# Full: winpodx core + reverse-open + GUI.
"$VENV_PY" -m pip install --quiet "${INSTALL_DIR}[gui,reverse-open]"
fi
# Belt-and-suspenders: ensure cairosvg + pyxdg are present even if a
# future pyproject reshuffle moves them out of the reverse-open extra.
# These two drive SVG / themed app-icon conversion for reverse-open.
if ! "$VENV_PY" -c "import cairosvg, xdg" >/dev/null 2>&1; then
log "Ensuring reverse-open icon deps (cairosvg + pyxdg) in the venv..."
"$VENV_PY" -m pip install --quiet "cairosvg>=2.7,<3.0" "pyxdg>=0.27,<1.0" || \
warn "cairosvg/pyxdg install into venv failed; SVG/themed icons will use a placeholder."
fi
# --- Load Windows container image from local tar (--image-tar) ---
# Runs AFTER the winpodx source is in place so the rest of the install
# can still proceed if the load fails (first-boot would pull from the
# registry as a fallback — warn but don't abort).
if [ -n "$WINPODX_IMAGE_TAR" ]; then
log "Loading Windows container image from $WINPODX_IMAGE_TAR..."
if command -v podman >/dev/null 2>&1; then
podman load -i "$WINPODX_IMAGE_TAR" || warn "image load failed; first boot may try the registry"
elif command -v docker >/dev/null 2>&1; then
docker load -i "$WINPODX_IMAGE_TAR" || warn "image load failed; first boot may try the registry"
else
warn "neither podman nor docker found; cannot load image tar"
fi
fi
# --- Create launcher script ---
# v2: exec the VENV's python, not system python3 + PYTHONPATH. No system
# python pollution; winpodx + its deps live entirely under the venv.
cat > "$LAUNCHER" << 'LAUNCHER_EOF'
#!/usr/bin/env bash
WINPODX_DIR="$HOME/.local/bin/winpodx-app"
exec "$WINPODX_DIR/.venv/bin/python" -m winpodx "$@"
LAUNCHER_EOF
chmod +x "$LAUNCHER"
# --- Create 'winpodx' command (symlink to launcher) ---
ln -sfn "$LAUNCHER" "$SYMLINK"
# Ensure ~/.local/bin is in PATH
if ! echo "$PATH" | grep -q "$HOME/.local/bin"; then
warn "$HOME/.local/bin is not in PATH"
warn "Add this to your ~/.bashrc or ~/.zshrc:"
warn ' export PATH="$HOME/.local/bin:$PATH"'
fi
# --- Run setup ---
# --manual / WINPODX_MANUAL=1: skip setup + the entire provisioning
# chain below (wait-ready / migrate / discovery / reverse-open). The
# binary + venv are installed, but the Windows VM stays unprovisioned
# until the user picks one of the first-run prompt options on the next
# `winpodx` invocation (CLI Y/C/n or GUI modal -- #255 PR 1).
if [ "${WINPODX_MANUAL:-0}" = "1" ]; then
log "Manual mode (--manual / WINPODX_MANUAL=1 / --backend manual) — skipping setup + Windows provisioning."
log " Run 'winpodx setup' to finish setup (or 'winpodx gui' for the graphical first-run with auto / customize / skip)."
else
log "Running winpodx setup..."
# 0.6.0 item B: --create-only is gone. setup writes config + creates the
# container; the post-create chain runs once via `winpodx provision`
# (below) — the single source of truth shared with setup_cmd / migrate /
# pending.resume. WINPODX_NO_PROVISION makes setup itself skip its own
# full-provision tail so install.sh's explicit `winpodx provision` is the
# only run (setup would otherwise also run finish_provisioning).
SETUP_ARGS=(--non-interactive)
if [ -n "$WINPODX_BACKEND" ] && [ "$WINPODX_BACKEND" != "manual" ]; then
SETUP_ARGS+=(--backend "$WINPODX_BACKEND")
log "Backend: $WINPODX_BACKEND"
fi
if [ -n "$WINPODX_WIN_VERSION" ]; then
SETUP_ARGS+=(--win-version "$WINPODX_WIN_VERSION")
log "Installing Windows edition: $WINPODX_WIN_VERSION"
fi
if [ -n "$WINPODX_STORAGE_DIR" ]; then
SETUP_ARGS+=(--storage-path "$WINPODX_STORAGE_DIR")
log "Storage location: $WINPODX_STORAGE_DIR"
fi
# #647: hand the local ISO to setup so it stages <storage>/custom.iso
# BEFORE compose-up (dockur needs it present when the container boots).
if [ -n "$WINPODX_WIN_ISO" ]; then
SETUP_ARGS+=(--win-iso "$WINPODX_WIN_ISO")
log "Local Windows ISO: $WINPODX_WIN_ISO"
fi
# Persist the resolved FreeRDP source so the launcher honours it
# (cfg.rdp.freerdp_source). Skip "auto" — that's the default.
if [ -n "${WINPODX_FREERDP_SOURCE:-}" ] && [ "$WINPODX_FREERDP_SOURCE" != "auto" ]; then
SETUP_ARGS+=(--freerdp-source "$WINPODX_FREERDP_SOURCE")
log "FreeRDP source: $WINPODX_FREERDP_SOURCE"
fi
# WINPODX_NO_PROVISION=1: setup creates the container but skips its own
# full-provision tail; install.sh runs the chain once via the explicit
# `winpodx provision` call below (0.6.0 item B, replaces --create-only).
WINPODX_NO_PROVISION=1 "$VENV_PY" -m winpodx setup "${SETUP_ARGS[@]}" 2>/dev/null || true
fi
# NOTE: --win-iso staging now happens INSIDE `winpodx setup` (passed via
# SETUP_ARGS above), before compose-up, so dockur finds custom.iso when the
# container boots (#647). It used to be staged here, after setup had already
# started the container + dockur had begun downloading — too late.
# --- Install winpodx GUI desktop entry & icon ---
mkdir -p "$DESKTOP_DIR" "$ICON_DIR"
cp "$INSTALL_DIR/data/winpodx.desktop" "$DESKTOP_DIR/winpodx.desktop"
cp "$INSTALL_DIR/data/winpodx-icon.svg" "$ICON_DIR/winpodx.svg"
# Ensure index.theme exists (required for KDE icon cache)
if [ ! -f "$ICON_BASE/index.theme" ]; then
if [ -f /usr/share/icons/hicolor/index.theme ]; then
cp /usr/share/icons/hicolor/index.theme "$ICON_BASE/index.theme"
else
cat > "$ICON_BASE/index.theme" << 'INDEXEOF'
[Icon Theme]
Name=Hicolor
Comment=Fallback icon theme
Hidden=true
Directories=scalable/apps
[scalable/apps]
Size=64
MinSize=1
MaxSize=512
Context=Applications
Type=Scalable
INDEXEOF
fi
fi
gtk-update-icon-cache -f -t "$ICON_BASE" 2>/dev/null || true
# Rebuild KDE Plasma sycoca cache
kbuildsycoca6 --noincremental 2>/dev/null || kbuildsycoca5 --noincremental 2>/dev/null || true
log "Installed WinPodX GUI launcher and icon"
# v0.1.9: bundled app profiles were dropped. The app menu now populates
# automatically the first time the Windows pod boots — `winpodx app run
# desktop` starts the pod, the provisioner auto-fires discovery, and the
# discovered apps + their real Windows-extracted icons land in the menu.
# Manual trigger any time: `winpodx app refresh`.
# In manual mode the provisioning chain below is skipped entirely.
if [ "${WINPODX_MANUAL:-0}" = "1" ]; then
ROLLBACK_ARMED=0
cleanup_install_marker
trap - EXIT ERR INT TERM
echo ""
echo " Location: $INSTALL_DIR"
echo " Command: winpodx"
echo ""
echo " Manual mode — Windows VM was NOT provisioned."
echo ""
echo " Next step (pick one):"
echo " winpodx gui # GUI first-run modal: auto / customize / skip"
echo " winpodx setup # Run setup directly (non-interactive auto)"
echo " winpodx setup --customize"
echo " # Run setup wizard (pick every knob)"
echo ""
log "Installation complete!"
exit 0
fi
# --- Finish provisioning the Windows VM (0.6.0 item B) ---
# The wait-ready → agent-settle → apply-fixes → discovery → reverse-open
# chain used to be ~140 lines of bash here (with its own /health curl poll,
# 6× `app refresh` retry loop, and host-open listener-start). It is now the
# single `winpodx provision` command — the same chain setup_cmd, migrate,
# and pending.resume run — so there is exactly one place to fix a bug and
# one shared progress surface. install.sh just forwards $WINPODX_VERBOSE.
#
# Fresh vs upgrade are DIFFERENT flows (a blind `provision`-for-both was the
# regression in the first cut of item B):
#
# * FRESH (no prior config): `winpodx provision --require-agent`. The new
# container's install.bat already laid down the current guest scripts, so
# no guest_sync is needed. --require-agent (#271) makes discovery/apply
# refuse the FreeRDP fallback and DEFER (exit 5 -> pending) rather than
# race FreeRDP into install.bat's autologon session while the agent is
# still flapping during first boot.
#
# * UPGRADE (prior config existed): `winpodx migrate`. migrate FIRST pushes
# the refreshed guest scripts into the existing guest (guest_sync) + pins
# the image + shows release notes, THEN runs the same apply -> discovery ->
# reverse-open chain. Skipping migrate (as the first item-B cut did) left
# upgraded guests running STALE agent.ps1 / OEM scripts.
#
# Both commands stream the live boot progress (the #126 wget-ETA dynamic
# deadline + self-erasing line) and both honour the .pending_setup safety net.
#
# Exit codes (both):
# 0 — chain succeeded
# 4 — Windows guest didn't become responsive in time (wait-ready timeout)
# 5 — deferred (agent-first: agent never came up; resume via pending)
# 4 and 5 are non-fatal: the .pending_setup machinery (utils/pending.py) is the
# safety net — the next `winpodx` CLI / GUI launch resumes the chain.
# Skip the whole step with WINPODX_NO_WAIT=1 (CI / non-interactive setups).
PENDING_FILE="$HOME/.config/winpodx/.pending_setup"
if [ -f "$HOME/.config/winpodx/winpodx.toml" ] && [ "${WINPODX_NO_WAIT:-}" != "1" ]; then
PROVISION_OUT="$(mktemp)"
if [ "$IS_FRESH_INSTALL" = "1" ]; then
log "Finishing Windows provisioning (wait-ready + apply-fixes + discovery + reverse-open)..."
log " Fresh install downloads ~7.5GB Windows ISO + runs Sysprep + OEM apply (auto-extends on slow links)."
log " Subsequent installs reuse the cached ISO and finish in 2-5 min."
PROVISION_CMD=(provision --require-agent)
[ -n "$WINPODX_VERBOSE" ] && PROVISION_CMD+=(--verbose)
else
log "Upgrade detected — running migration (sync guest scripts + apply-fixes + discovery + reverse-open)..."
log " Re-uses the existing Windows install; no ISO re-download."
PROVISION_CMD=(migrate --non-interactive)
[ -n "$WINPODX_VERBOSE" ] && PROVISION_CMD+=(--verbose)
fi
# PYTHONUNBUFFERED=1 keeps the streamed per-stage progress line-buffered
# when piped. We inspect the rc explicitly and tee the output so the
# `no such container` partial-uninstall case is still detectable.
#
# Disarm the ERR-trap rollback around this call. `set +e` alone is NOT
# enough: bash fires the ERR trap on a failing *pipeline* even with
# errexit off, so a deferred (exit 5) or slow (exit 4) provision —
# where Windows is already downloaded, booted, and recoverable via the
# pending machinery — would otherwise roll back the whole fresh install
# before the rc handling below ever runs. We branch on the rc ourselves.
set +e
trap - ERR
PYTHONUNBUFFERED=1 "$SYMLINK" "${PROVISION_CMD[@]}" 2>&1 \
| tee "$PROVISION_OUT"
PROVISION_RC="${PIPESTATUS[0]}"
trap rollback_and_exit_err ERR
set -e
# Ctrl+C / SIGTERM: bail out. The traps fire in the parent shell too, but
# this covers the piped-install case where the child died from the signal
# and the parent didn't see it directly.
if [ "$PROVISION_RC" -eq 130 ] || [ "$PROVISION_RC" -eq 143 ]; then
err "Install cancelled (winpodx ${PROVISION_CMD[0]} returned $PROVISION_RC)."
err "Re-run install.sh to continue from where you left off."
rm -f "$PROVISION_OUT"
ROLLBACK_ARMED=0
cleanup_install_marker
trap - EXIT ERR INT TERM
exit "$PROVISION_RC"
fi
if [ "$PROVISION_RC" -eq 4 ] || [ "$PROVISION_RC" -eq 5 ]; then
# DEFERRED, not failed. Windows is downloaded, booted, and the pod is
# up; only the agent-first discovery deferred (5) or wait-ready ran
# long (4). Rolling back here would throw away ~15 min of ISO download
# + boot for a state that finishes itself. Record the remaining steps
# as pending — they auto-resume on the next `winpodx` invocation — and
# treat the install as a SUCCESS so the artifacts stay in place.
warn "Windows is installed and the pod is up, but app discovery deferred"
warn "until the in-guest agent finishes coming up (it can lag a minute"
warn "after the first-boot reboot). This is not a failure — it finishes"
warn "automatically on your next \`winpodx\` run, or force it now with:"
warn " winpodx app refresh"
mkdir -p "$HOME/.config/winpodx"
printf 'wait_ready\nmigrate\ndiscovery\n' > "$PENDING_FILE"
warn "Pending steps recorded at $PENDING_FILE."
elif [ "$PROVISION_RC" -ne 0 ]; then
if grep -q "no such container" "$PROVISION_OUT"; then
warn "Container is missing — likely from a partial uninstall."
warn "Recover with a full reinstall:"
warn ' curl -fsSL https://raw.githubusercontent.com/kernalix7/winpodx/main/uninstall.sh | bash -s -- --purge'
warn ' curl -fsSL https://raw.githubusercontent.com/kernalix7/winpodx/main/install.sh | bash -s -- --main'
else
warn "Provisioning didn't complete in time (Windows first-boot can run long"
warn "on slow links -- see #126). Marking remaining steps as pending — they"
warn "auto-resume on the next \`winpodx\` CLI invocation or when you open the GUI."
mkdir -p "$HOME/.config/winpodx"
# wait_ready first so pending.resume re-runs the full chain.
printf 'wait_ready\nmigrate\ndiscovery\n' > "$PENDING_FILE"
warn "Pending steps recorded at $PENDING_FILE."
fi
else
rm -f "$PENDING_FILE"
fi
rm -f "$PROVISION_OUT"
fi
# --- Done. Disarm rollback (success). ---
ROLLBACK_ARMED=0
cleanup_install_marker
trap - EXIT ERR INT TERM
INSTALLED_VER="$("$VENV_PY" -c 'import winpodx; print(winpodx.__version__)' 2>/dev/null || echo '?')"
GUI_LABEL="yes"
[ -n "$WINPODX_NO_GUI" ] && GUI_LABEL="no (--no-gui)"
# Apply the update to an already-RUNNING WinPodX immediately. A long-lived tray
# or GUI keeps the OLD code in memory, so on an upgrade the new code otherwise
# only takes effect after the user manually restarts or re-logs in (this caused
# repeated "the fix didn't work" confusion). Restart ONLY the winpodx APP
# processes by targeting the 'tray' / 'gui' entrypoints precisely.
#
# NEVER use a bare `pkill -f winpodx`: that also matches conmon (the container
# monitor for the 'winpodx-windows' pod) and would tear down the running
# Windows VM + every live RDP session. The 'tray'/'gui' patterns below do not
# match conmon, the QEMU process, or this installer's own provision/migrate run.
# A fresh install has nothing running here, so this is a no-op.
if command -v pgrep >/dev/null 2>&1; then
_wpx_was_tray=0
_wpx_was_gui=0
pgrep -f 'winpodx tray' >/dev/null 2>&1 && _wpx_was_tray=1
pgrep -f 'winpodx gui' >/dev/null 2>&1 && _wpx_was_gui=1
if [ "$_wpx_was_tray" = 1 ] || [ "$_wpx_was_gui" = 1 ]; then
log "Restarting WinPodX (tray/GUI) to apply the update (the pod keeps running)…"
pkill -f 'winpodx tray' 2>/dev/null || true
pkill -f 'winpodx gui' 2>/dev/null || true
sleep 1
# If the GUI was up, relaunch it (the GUI auto-spawns its own tray);
# otherwise just relaunch the tray. Avoids a double tray.
if [ "$_wpx_was_gui" = 1 ]; then
setsid "$SYMLINK" gui >/dev/null 2>&1 </dev/null &
elif [ "$_wpx_was_tray" = 1 ]; then
setsid "$SYMLINK" tray >/dev/null 2>&1 </dev/null &
fi
fi
fi
echo ""
echo -e "${GREEN} ┌─ WinPodX installed ──────────────────────────────────────${NC}"
printf " │ version %s\n" "$INSTALLED_VER"
printf " │ backend %s\n" "${WINPODX_BACKEND:-podman}"
printf " │ GUI %s\n" "$GUI_LABEL"
printf " │ location %s\n" "$INSTALL_DIR (private venv)"
echo -e "${GREEN} └──────────────────────────────────────────────────────────${NC}"
echo ""
echo " Next steps:"
if [ -z "$WINPODX_NO_GUI" ]; then
echo " winpodx gui # open the GUI — system tray + app manager"
fi
echo " winpodx app run desktop # the full Windows desktop"
echo " winpodx app refresh # rescan the apps installed in Windows"
echo " winpodx doctor # health check if something looks off"
echo " winpodx --help # every command"
echo ""
echo " Your Windows apps are already in the application menu — click any one"
echo " and WinPodX handles the pod, the RDP session, and the window for you."
echo ""
log "Installation complete!"