AnyBot/install.sh

367 lines
18 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/sh
# AnyBot 一鍵安裝。什麼都沒裝的電腦也可以:會自己裝好需要的東西(系統套件、Rust、Docker)。
#
# curl -fsSL https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh | sh
# 桌面 App(Mac、有螢幕的 Linux)。沒有螢幕的 Linux 會自動改成「手腳」
# … | sh -s -- hands [網址 配對碼]
# 沒有螢幕的 Linux:裝好後接成大腦的手腳(網址和配對碼在 AnyBot 的「加一台電腦」)
# … | sh -s -- server
# 伺服器:裝 Docker,架好帳號伺服器 + 大腦 + HTTPS(只問網域、帳號、密碼)
# … | sh -s -- deps
# 只裝建置需要的東西(之後自己 make)
#
# 已經 clone 下來的話,在資料夾裡執行 ./install.sh [app|hands|server|deps],效果一樣。
# 環境變數:ANYBOT_DIR(放原始碼的地方,預設 ~/AnyBot)、ANYBOT_BRANCH(預設 main)。
set -eu
REPO=${ANYBOT_REPO:-https://code.30cm.net/daniel.w/AnyBot.git}
BRANCH=${ANYBOT_BRANCH:-main}
DIR=${ANYBOT_DIR:-$HOME/AnyBot}
RUST_MIN=95 # Rust 1.95 以上
MODE=${1:-auto}
[ $# -gt 0 ] && shift
case $MODE in
app|hands|headless|server|deps|auto) ;;
-h|--help|help)
# 用 curl | sh 執行時 $0 是「sh」,讀不到腳本原文:改印內嵌的說明。
usage=$(sed -n '2,14p' "$0" 2>/dev/null | sed 's/^# \{0,1\}//' || true)
[ -n "$usage" ] || usage='AnyBot 一鍵安裝:
sh install.sh 桌面 App(Mac、有螢幕的 Linux)
sh install.sh hands [網址 配對碼] 沒有螢幕的 Linux,接成大腦的手腳
sh install.sh server 伺服器:帳號伺服器 + 大腦 + HTTPS(Docker)
sh install.sh deps 只裝建置需要的東西(之後自己 make)
環境變數:ANYBOT_DIR(原始碼位置,預設 ~/AnyBot)、ANYBOT_BRANCH(預設 main)。'
printf '%s\n' "$usage"; exit 0 ;;
*) echo "不認得「${MODE}」。可以用:app、hands、server、deps"; exit 1 ;;
esac
[ "$MODE" = headless ] && MODE=hands
# ---------- 外觀 ----------
if [ -t 1 ]; then B=$(printf '\033[1m'); D=$(printf '\033[90m'); G=$(printf '\033[32m'); Y=$(printf '\033[33m'); R=$(printf '\033[31m'); C=$(printf '\033[36m'); N=$(printf '\033[0m')
else B=; D=; G=; Y=; R=; C=; N=; fi
STEP=0; TOTAL=4
step() { STEP=$((STEP + 1)); printf '\n%s[%d/%d]%s %s%s%s\n' "$C" "$STEP" "$TOTAL" "$N" "$B" "$1" "$N"; }
ok() { printf ' %s✓%s %s\n' "$G" "$N" "$1"; }
info() { printf ' %s%s%s\n' "$D" "$1" "$N"; }
warn() { printf ' %s!%s %s\n' "$Y" "$N" "$1"; }
die() { printf '\n %s✗ %s%s\n' "$R" "$1" "$N"; [ $# -gt 1 ] && printf ' %s\n' "$2"; exit 1; }
have() { command -v "$1" >/dev/null 2>&1; }
# 互動輸入:用 curl | sh 時標準輸入是腳本本身,改從終端機讀。
TTY=/dev/tty
[ -r "$TTY" ] && [ -w "$TTY" ] && (exec <"$TTY") 2>/dev/null || TTY=
# PATH 提示要看的是使用者登入後的 PATH,不是腳本自己改過的(下面會加 ~/.local/bin)。
START_PATH=$PATH
# ---------- 這台是什麼 ----------
OS=$(uname -s); ARCH=$(uname -m)
case $OS in
Darwin) PLAT=mac ;;
Linux) PLAT=linux ;;
*) die "這個腳本只支援 macOS 和 Linux。" "Windows:照 docs/install.md 用 cargo build --release。" ;;
esac
if [ "$MODE" = auto ]; then
if [ $PLAT = linux ] && [ -z "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ] && ! have gnome-shell && ! have plasmashell && ! have startxfce4; then
MODE=hands
else
MODE=app
fi
fi
[ $PLAT = mac ] && [ "$MODE" = hands ] && MODE=app
SUDO=
if [ $PLAT = linux ] && [ "$(id -u)" != 0 ]; then
have sudo || die "需要 sudo 才能安裝系統套件。" "請用 root 執行,或先裝 sudo。"
SUDO=sudo
fi
PM=
if [ $PLAT = linux ]; then
for p in apt-get dnf pacman zypper; do have $p && { PM=$p; break; }; done
fi
case $MODE in
app) TITLE="桌面 App" ;;
hands) TITLE="沒有螢幕的電腦(手腳)" ;;
server) TITLE="伺服器(帳號伺服器 + 大腦 + HTTPS)"; TOTAL=3 ;;
deps) TITLE="只裝建置需要的東西"; TOTAL=2 ;;
esac
printf '\n %s◆ AnyBot 安裝%s %s%s · %s %s%s\n' "$C$B" "$N" "$D" "$TITLE" "$OS" "$ARCH" "$N"
# ---------- 系統套件 ----------
# apt:只裝這個發行版有的名字(例如 Ubuntu 24.04 的 libasound2 叫 libasound2t64)。
# 最後加 true:清單最後一個套件不存在時,最後一条 apt-cache 判斷會失敗,
# 在 set -e 下整個腳本會無訊息退出(呼叫端是 pkgs=$(apt_pick …))。
apt_pick() {
for p in "$@"; do
case $p in
*\|*) # 幾個名字選第一個有的
for alt in $(echo "$p" | tr '|' ' '); do
if apt-cache show "$alt" >/dev/null 2>&1; then echo "$alt"; break; fi
done ;;
*) apt-cache show "$p" >/dev/null 2>&1 && echo "$p" ;;
esac
done
true
}
# zypper:同 apt_pick,裝之前先過濾掉這台找不到的名字(SUSE 各版本套件名差異大,
# 一個名字不存在會讓整組安裝失敗)。
zypper_pick() {
for p in "$@"; do
zypper --non-interactive info "$p" >/dev/null 2>&1 && echo "$p"
done
true
}
linux_packages() {
# $1: build | app | hands | server
case $PM in
apt-get)
case $1 in
server) set -- curl ca-certificates git ;;
build) set -- build-essential pkg-config git curl ca-certificates libasound2-dev ;;
# 手腳:虛擬螢幕、視窗管理員、輔助使用服務(讀 App 的按鈕)、session bus,Bot 才看得到這台的畫面。
hands) set -- build-essential pkg-config git curl ca-certificates libasound2-dev "libasound2t64|libasound2" \
xvfb openbox at-spi2-core libatk-adaptor dbus-x11 dbus-user-session xauth fonts-noto-cjk ;;
app) set -- build-essential pkg-config git curl ca-certificates libasound2-dev "libasound2t64|libasound2" \
libxkbcommon-x11-0 "libgl1|libgl1-mesa-glx" xdg-utils libnotify-bin libsecret-tools espeak-ng zenity \
xclip wl-clipboard ffmpeg fonts-noto-cjk ;;
esac
# A fresh system has no package lists yet: fetch them first, or nothing can be picked.
if ! ls /var/lib/apt/lists/*_Packages >/dev/null 2>&1; then
$SUDO apt-get update -qq || warn "apt-get update 有錯誤,繼續試試看"
UPDATED=1
fi
pkgs=$(apt_pick "$@" | tr '\n' ' ')
# Already there (an update): nothing to install, no sudo password needed.
missing=""
for p in $pkgs; do dpkg -s "$p" >/dev/null 2>&1 || missing="$missing $p"; done
if [ -z "$missing" ]; then info "都已經裝好了"; return 0; fi
pkgs=$missing
[ -n "${UPDATED:-}" ] || $SUDO apt-get update -qq || warn "apt-get update 有錯誤,繼續試試看"
info "apt-get install$pkgs"
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq $pkgs >/dev/null || die "安裝系統套件失敗。" "請看上面的錯誤,或手動執行:sudo apt-get install $pkgs"
;;
dnf)
base="gcc gcc-c++ make pkgconf-pkg-config git curl ca-certificates alsa-lib-devel"
extra=""
[ "$1" = app ] && extra="libxkbcommon-x11 mesa-libGL xdg-utils libnotify libsecret espeak-ng zenity xclip wl-clipboard ffmpeg-free google-noto-sans-cjk-fonts"
[ "$1" = hands ] && extra="xorg-x11-server-Xvfb openbox at-spi2-core at-spi2-atk dbus-x11 xorg-x11-xauth google-noto-sans-cjk-fonts"
[ "$1" = server ] && base="curl git ca-certificates"
$SUDO dnf install -y -q $base $extra || die "安裝系統套件失敗。" "sudo dnf install $base $extra"
;;
pacman)
base="base-devel git curl alsa-lib"
[ "$1" = app ] && base="$base libxkbcommon-x11 mesa xdg-utils libnotify libsecret espeak-ng zenity xclip wl-clipboard ffmpeg noto-fonts-cjk"
[ "$1" = hands ] && base="$base xorg-server-xvfb openbox at-spi2-core dbus xorg-xauth noto-fonts-cjk"
[ "$1" = server ] && base="curl git"
$SUDO pacman -S --needed --noconfirm $base || die "安裝系統套件失敗。" "sudo pacman -S --needed $base"
;;
zypper)
base="gcc gcc-c++ make pkg-config git curl alsa-devel"
extra=""
# app/hands 也要有這台该有的套件(之前只有 base,桌面模式缺 xkbcommon/GL/通知…,
# 編譯或執行時才炸)。名字逐個過濾,找不到的跳過。
[ "$1" = app ] && extra="libxkbcommon-x11-0 Mesa libGL1 xdg-utils libnotify-tools libnotify4 libsecret-1-0 espeak zenity xclip wl-clipboard ffmpeg fonts-noto"
[ "$1" = hands ] && extra="xorg-x11-server-Xvfb openbox at-spi2-core at-spi2-atk-common dbus-1-x11 xauth fonts-noto"
[ "$1" = server ] && base="curl git"
pkgs=$(zypper_pick $base $extra)
$SUDO zypper -n install $pkgs || die "安裝系統套件失敗。" "sudo zypper install $pkgs"
;;
*)
die "認不出這台的套件管理程式(apt、dnf、pacman、zypper 都沒有)。" \
"請自己裝:C 編譯器、pkg-config、git、curl、ALSA 開發套件,再執行一次。"
;;
esac
}
mac_tools() {
if xcode-select -p >/dev/null 2>&1; then ok "Xcode Command Line Tools"; return; fi
warn "需要 Xcode Command Line Tools,正在打開安裝視窗…"
xcode-select --install 2>/dev/null || true
die "裝好之後(約 5 分鐘),再執行一次同一行指令。"
}
# ---------- Rust ----------
rust_ok() {
have cargo && have rustc || return 1
# 抓「主版號 次版號」:舊寫法死鎖在 1.x,rustc 2.x 會被当成「太舊」。
set -- $(rustc --version 2>/dev/null | sed -n 's/^rustc \([0-9][0-9]*\)\.\([0-9][0-9]*\).*/\1 \2/p')
[ $# -ge 2 ] || return 1
[ "$1" -gt 1 ] && return 0
[ "$1" -eq 1 ] && [ "$2" -ge "$RUST_MIN" ]
}
ensure_rust() {
[ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"
if rust_ok; then ok "Rust $(rustc --version | cut -d' ' -f2)"; return; fi
if have rustup; then
info "Rust 太舊,更新中(rustup update stable)…"
rustup update stable >/dev/null 2>&1 && rustup default stable >/dev/null 2>&1 || true
# rustup 裝在 ~/.cargo/bin;這個終端機可能還沒有它(--no-modify-path 裝的)。
[ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"
else
if have rustc; then warn "系統的 Rust $(rustc --version | cut -d' ' -f2) 太舊(要 1.$RUST_MIN 以上),另外用 rustup 裝新版"; fi
info "安裝 Rust(rustup,裝在 ~/.cargo,不用 root)…"
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q --profile minimal --no-modify-path >/dev/null 2>&1 \
|| die "安裝 Rust 失敗。" "手動:curl --proto '=https' -sSf https://sh.rustup.rs | sh"
. "$HOME/.cargo/env"
fi
rust_ok || die "Rust 還是太舊:$(rustc --version 2>/dev/null)" "執行 rustup update stable,或移除系統的 rustc/cargo 套件。"
ok "Rust $(rustc --version | cut -d' ' -f2)"
case ":$PATH:" in *":$HOME/.cargo/bin:"*) ;; *)
for rc in "$HOME/.profile" "$HOME/.bashrc" "$HOME/.zshrc"; do
[ -f "$rc" ] && ! grep -q '.cargo/env' "$rc" && echo '. "$HOME/.cargo/env"' >>"$rc"
done ;;
esac
}
# ---------- 原始碼 ----------
SRC=
find_source() {
here=$(cd "$(dirname "$0")" 2>/dev/null && pwd || true)
if [ -n "$here" ] && [ -f "$here/Cargo.toml" ] && grep -q '^name = "anybot"' "$here/Cargo.toml"; then
SRC=$here; ok "用這份原始碼:$SRC"; return
fi
have git || die "需要 git。"
if [ -d "$DIR/.git" ]; then
info "更新 $DIR …"
git -C "$DIR" pull --ff-only -q || warn "沒辦法更新(你改過裡面的檔案?),用現有的版本"
else
[ -e "$DIR" ] && die "$DIR 已經存在,但不是 AnyBot 的原始碼。" "換個地方:ANYBOT_DIR=~/別的資料夾 再執行一次。"
info "下載原始碼到 $DIR …"
git clone -q --depth 1 -b "$BRANCH" "$REPO" "$DIR" || die "下載原始碼失敗。" "網路連得到 $REPO 嗎?"
fi
SRC=$DIR; ok "原始碼:${SRC}($(git -C "$SRC" log -1 --format=%h))"
}
# 記憶體小的機器同時編太多會被系統砍掉:每 2 GB 一個工作。
build_jobs() {
mem_mb=0
if [ -r /proc/meminfo ]; then
mem_mb=$(awk '/MemTotal/ {m=$2} /SwapTotal/ {s=$2} END {print int((m + s / 2) / 1024)}' /proc/meminfo)
fi
[ "$mem_mb" -gt 0 ] || { echo ""; return; }
j=$((mem_mb / 2000)); [ $j -lt 1 ] && j=1
cpus=$(nproc 2>/dev/null || echo $j); [ $j -gt "$cpus" ] && j=$cpus
echo $j
if [ "$mem_mb" -lt 3000 ]; then
warn "這台記憶體只有 ${mem_mb} MB,編譯會比較久(一次編一個)。若被中斷,加 2 GB swap 再試:" >&2
info " sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile" >&2
fi
}
build() {
jobs=$(build_jobs)
info "編譯中,第一次約 5–15 分鐘(之後更新只重編改過的部分)…"
log="$SRC/target/install-build.log"; mkdir -p "$SRC/target"
if (cd "$SRC" && cargo build --release --bin anybot ${jobs:+-j $jobs}) >"$log" 2>&1; then
ok "編譯完成"
else
tail -n 25 "$log" | sed 's/^/ /'
die "編譯失敗(完整紀錄:${log})" "把紀錄的最後幾行貼給維護的人;常見原因見 docs/install.md 的「常見問題」。"
fi
}
# ---------- Docker(伺服器)----------
ensure_docker() {
if ! have docker; then
info "安裝 Docker(官方腳本 get.docker.com)…"
curl -fsSL https://get.docker.com | $SUDO sh >/dev/null || die "安裝 Docker 失敗。" "手動:https://docs.docker.com/engine/install/"
fi
$SUDO systemctl enable --now docker >/dev/null 2>&1 || true
docker compose version >/dev/null 2>&1 || $SUDO docker compose version >/dev/null 2>&1 || {
case $PM in
apt-get) $SUDO apt-get install -y -qq docker-compose-plugin >/dev/null 2>&1 || $SUDO apt-get install -y -qq docker-compose-v2 >/dev/null 2>&1 || true ;;
dnf) $SUDO dnf install -y -q docker-compose-plugin >/dev/null 2>&1 || true ;;
esac
}
docker compose version >/dev/null 2>&1 || $SUDO docker compose version >/dev/null 2>&1 || die "沒有 docker compose。" "請更新 Docker:https://docs.docker.com/compose/install/"
ok "Docker $(docker --version 2>/dev/null | sed 's/Docker version \([^,]*\).*/\1/')"
if ! docker info >/dev/null 2>&1; then
if [ -n "$SUDO" ]; then
$SUDO usermod -aG docker "$(id -un)" 2>/dev/null || true
DOCKER_SUDO=1
info "已把你加進 docker 群組(下次登入後不用 sudo);這次先用 sudo。"
fi
fi
}
# =====================================================================
case $MODE in
deps)
step "系統套件"
if [ $PLAT = mac ]; then mac_tools; else linux_packages app; ok "系統套件"; fi
step "Rust"
ensure_rust
printf '\n %s好了。%s在原始碼資料夾裡:%smake%s 建置、%smake install%s 安裝。\n' "$G$B" "$N" "$B" "$N" "$B" "$N"
[ -n "${PS1:-}" ] || info "這個終端機還找不到 cargo 的話:. \"\$HOME/.cargo/env\""
;;
server)
[ $PLAT = linux ] || die "伺服器模式要在 Linux 上跑。" "Mac 當大腦的做法見 docs/multi-device.md。"
step "系統套件和 Docker"
linux_packages server
ensure_docker
step "原始碼"
find_source
step "設定和啟動"
info "接下來會問網域、帳號、密碼。"
cd "$SRC/deploy/server"
IN=$TTY
if [ -n "${ANYBOT_SETUP_USER:-}" ] && [ -n "${ANYBOT_SETUP_PASSWORD:-}" ]; then IN=/dev/null # 免問模式
else [ -n "$TTY" ] || die "需要一個終端機回答問題。" "在伺服器上直接執行:cd $SRC && ./install.sh server(或用 ANYBOT_SETUP_USER、ANYBOT_SETUP_PASSWORD 免問)"; fi
if [ "${DOCKER_SUDO:-0}" = 1 ]; then exec $SUDO -E ./setup.sh "$@" <"$IN"; else exec ./setup.sh "$@" <"$IN"; fi
;;
app|hands)
step "系統套件"
if [ $PLAT = mac ]; then mac_tools
elif [ "$MODE" = hands ] && have anybot && [ $# -gt 0 ]; then ok "AnyBot 已經裝好,跳過"
else linux_packages $MODE; ok "系統套件"; fi
if [ "$MODE" = hands ] && have anybot && [ $# -gt 0 ]; then
STEP=3; info "已經有 $(command -v anybot),直接接上。(要更新:不帶網址再執行一次)"
else
step "Rust"
ensure_rust
step "下載和編譯"
find_source
build
step "安裝"
if [ $PLAT = mac ]; then
(cd "$SRC" && make install >/dev/null) || die "安裝失敗。" "在 $SRC 執行 make install 看錯誤。"
ok "AnyBot.app 放在 ~/Applications"
else
sh "$SRC/deploy/linux/install.sh" >/dev/null || die "安裝失敗。" "執行 sh $SRC/deploy/linux/install.sh 看錯誤。"
ok "程式放在 ~/.local/bin/anybot"
[ "$MODE" = app ] && ok "應用程式選單裡有 AnyBot"
# 更新: the one running in the background (anybot setup's service) switches to the new one.
if [ -f "$HOME/.local/bin/anybot-run" ] && systemctl --user is-enabled --quiet anybot 2>/dev/null; then
"$HOME/.local/bin/anybot" setup --refresh-service >/dev/null 2>&1 && ok "重新啟動背景執行的 AnyBot(換成新版)"
fi
fi
fi
PATH="$HOME/.local/bin:$PATH"
if [ "$MODE" = hands ]; then
if [ $# -gt 0 ]; then
printf '\n'
[ -n "$TTY" ] && exec anybot setup "$@" <"$TTY"
exec anybot setup "$@"
fi
printf '\n %s裝好了。%s接著把這台接到你的大腦:\n' "$G$B" "$N"
printf ' 1. 在手機或電腦的 AnyBot 按「加一台電腦」,取得配對碼\n'
printf ' 2. 在這裡執行:%sanybot setup 網址 配對碼%s\n' "$B" "$N"
else
printf '\n %s裝好了。%s' "$G$B" "$N"
if [ $PLAT = mac ]; then printf '打開 ~/Applications/AnyBot(第一次:按右鍵 › 打開)。\n'
else printf '從應用程式選單打開 AnyBot,或在終端機輸入 %sanybot%s。\n' "$B" "$N"; fi
if ! have docker; then info "Bot 自己的電腦(一個隔離的桌面)需要 Docker,要用時 AnyBot 會告訴你怎麼裝。"; fi
info "要更新:再執行一次同一行指令。"
fi
case ":$START_PATH:" in *":$HOME/.local/bin:"*) ;; *) [ $PLAT = linux ] && info "(終端機找不到 anybot 的話,重新登入,或把 ~/.local/bin 加進 PATH)" ;; esac
;;
esac