316 lines
15 KiB
Bash
Executable File
316 lines
15 KiB
Bash
Executable File
#!/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) sed -n '2,14p' "$0" 2>/dev/null | sed 's/^# \{0,1\}//'; 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=
|
||
|
||
# ---------- 這台是什麼 ----------
|
||
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)。
|
||
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
|
||
}
|
||
|
||
linux_packages() {
|
||
# $1: build | app | hands | server
|
||
case $PM in
|
||
apt-get)
|
||
$SUDO apt-get update -qq || warn "apt-get update 有錯誤,繼續試試看"
|
||
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
|
||
pkgs=$(apt_pick "$@" | tr '\n' ' ')
|
||
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"
|
||
[ "$1" = server ] && base="curl git"
|
||
$SUDO zypper -n install $base || die "安裝系統套件失敗。" "sudo zypper install $base"
|
||
;;
|
||
*)
|
||
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
|
||
minor=$(rustc --version 2>/dev/null | sed -n 's/^rustc 1\.\([0-9]*\).*/\1/p')
|
||
[ -n "$minor" ] && [ "$minor" -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
|
||
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 "接下來會問網域、帳號、密碼。"
|
||
[ -n "$TTY" ] || die "需要一個終端機回答問題。" "在伺服器上直接執行:cd $SRC && ./install.sh server"
|
||
cd "$SRC/deploy/server"
|
||
if [ "${DOCKER_SUDO:-0}" = 1 ]; then exec $SUDO -E ./setup.sh "$@" <"$TTY"; else exec ./setup.sh "$@" <"$TTY"; 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"
|
||
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 ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) [ $PLAT = linux ] && info "(終端機找不到 anybot 的話,重新登入,或把 ~/.local/bin 加進 PATH)" ;; esac
|
||
;;
|
||
esac
|