#!/bin/sh # LOCUS 官网安装入口。先识别这台机器,选得出包才下载。 # curl -fsSL https://locus.casa/releases/install | sh # 只看这台机器能不能装,不下载、不需要 root: # curl -fsSL https://locus.casa/releases/install | sh -s -- diagnose # curl -fsSL https://locus.casa/releases/install | sh -s -- diagnose --json # 把参数交给安装器: # curl -fsSL https://locus.casa/releases/install | sh -s -- install -y # 固定版本: # LOCUS_VERSION=0.1.0 curl -fsSL https://locus.casa/releases/install | sh # # 仓库里的这份直接引用 ../installer/detect.sh;packaging/build-installer.sh 构建时把它内联进来, # 并填入本次发行的版本号和包清单(targets)。 set -eu BASE="${LOCUS_RELEASE_BASE:-https://locus.casa/releases}" VERSION="${LOCUS_VERSION:-}" ENTRY_VERSION="0.1.0.000065" ENTRY_TARGETS="linux-x86_64.tar.gz x86_64 glibc 2.34 3.2 verified;linux-aarch64.tar.gz aarch64 glibc 2.34 3.2 verified;linux-x86_64-musl.tar.gz x86_64 static - 3.2 experimental;linux-aarch64-musl.tar.gz aarch64 static - 3.2 experimental;linux-armv7-musleabihf.tar.gz armv7 static - 3.2 experimental" case $ENTRY_VERSION in @*@) ENTRY_VERSION="" ;; esac case $ENTRY_TARGETS in @*@) ENTRY_TARGETS="" ;; esac # shellcheck shell=sh # LOCUS 环境识别与选包。官网入口(sh)和内嵌安装器(bash)共用这一份,构建时原样内联进两者。 # # 只读文件、只调系统自带的命令(uname、id、getconf、ldd、df、docker),不执行下载来的程序, # 不需要 root。不读 MAC、序列号和网络配置。要在 bash 的 set -euo pipefail 下也能跑, # 所以每个可能失败的命令都带兜底。 # # 发行清单(targets)每行六列,用 ; 连成一行传进来: # <文件名> <架构> <最低 libc 版本> <最低内核> <状态> # linux-x86_64.tar.gz x86_64 glibc 2.34 3.2 verified # libc 取 glibc 或 static;static 不看系统 libc。状态取 verified 或 experimental。 # # LD_ROOT 只给测试用:所有文件都从它下面读。 LD_SCHEMA=1 LD_KERNEL_MIN=3.2 LD_DOCKER_ARCHES="x86_64 aarch64" # 还没有 targets 清单的发行版(0.1.0.000037 及以前)。 LD_LEGACY_TARGETS="linux-x86_64.tar.gz x86_64 glibc 2.34 3.2 verified;linux-aarch64.tar.gz aarch64 glibc 2.34 3.2 experimental" ld_t() { if [ "${LD_LANG:-en}" = zh ]; then printf '%s' "$1"; else printf '%s' "$2"; fi; } ld_have() { command -v "$1" >/dev/null 2>&1; } ld_path() { printf '%s%s' "${LD_ROOT:-}" "$1"; } # ld_ver_ge A B:A >= B,逐段按数字比较。不用 sort -V,busybox 的 sort 可能没有。 ld_ver_ge() { awk -v a="$1" -v b="$2" 'BEGIN { na = split(a, x, "."); nb = split(b, y, "."); n = na > nb ? na : nb for (i = 1; i <= n; i++) { p = x[i] + 0; q = y[i] + 0; if (p > q) exit 0; if (p < q) exit 1 } exit 0 }' } # KEY=value 或 KEY="value" 的文件(os-release、DSM 的 VERSION 和 synoinfo.conf)。 ld_kv() { [ -r "$1" ] || return 0 sed -n "s/^$2=//p" "$1" 2>/dev/null | head -n 1 | sed 's/^"\(.*\)"[[:space:]]*$/\1/' || : } # [节] 下的 键 = 值(QNAP 的 uLinux.conf)。 ld_ini() { [ -r "$1" ] || return 0 awk -v s="[$2]" -v k="$3" ' { l = $0; sub(/[ \t\r]+$/, "", l) } l ~ /^\[/ { in_s = (l == s); next } in_s { i = index(l, "="); if (!i) next key = substr(l, 1, i - 1); gsub(/^[ \t]+|[ \t]+$/, "", key) if (key == k) { v = substr(l, i + 1); gsub(/^[ \t]+/, "", v); print v; exit } }' "$1" 2>/dev/null || : } # ---------- 系统 ---------- ld_detect_os() { LD_KERNEL_NAME=$(uname -s 2>/dev/null) || LD_KERNEL_NAME="" LD_OS_FAMILY=linux LD_OS_NAME="" LD_OS_VERSION="" LD_VENDOR="" LD_MODEL="" LD_PLATFORM="" if [ "$LD_KERNEL_NAME" != Linux ]; then LD_OS_FAMILY=other LD_OS_NAME=${LD_KERNEL_NAME:-unknown} return 0 fi if [ -e "$(ld_path /etc.defaults/VERSION)" ]; then LD_OS_FAMILY=dsm LD_OS_NAME=DSM LD_VENDOR=Synology LD_OS_VERSION=$(ld_kv "$(ld_path /etc.defaults/VERSION)" productversion) if [ -z "$LD_OS_VERSION" ]; then LD_OS_VERSION=$(ld_kv "$(ld_path /etc.defaults/VERSION)" majorversion) [ -n "$LD_OS_VERSION" ] && LD_OS_VERSION="$LD_OS_VERSION.$(ld_kv "$(ld_path /etc.defaults/VERSION)" minorversion)" fi LD_MODEL=$(ld_kv "$(ld_path /etc/synoinfo.conf)" upnpmodelname) LD_PLATFORM=$(ld_kv "$(ld_path /etc/synoinfo.conf)" unique | awk -F_ '{print $2}') || LD_PLATFORM="" elif [ -e "$(ld_path /etc/config/uLinux.conf)" ]; then LD_OS_FAMILY=qnap LD_OS_NAME=QTS LD_VENDOR=QNAP if awk '$2 ~ /^\/share\// && $3 == "zfs" { f = 1 } END { exit !f }' "$(ld_path /proc/mounts)" 2>/dev/null; then LD_OS_NAME="QuTS hero" fi LD_MODEL=$(ld_ini "$(ld_path /etc/config/uLinux.conf)" System Model) LD_OS_VERSION=$(ld_ini "$(ld_path /etc/config/uLinux.conf)" System Version) elif [ -r "$(ld_path /etc/os-release)" ]; then LD_OS_NAME=$(ld_kv "$(ld_path /etc/os-release)" PRETTY_NAME) [ -n "$LD_OS_NAME" ] || LD_OS_NAME=$(ld_kv "$(ld_path /etc/os-release)" NAME) LD_OS_VERSION=$(ld_kv "$(ld_path /etc/os-release)" VERSION_ID) fi return 0 } ld_detect_kernel() { LD_KERNEL_RELEASE=$(uname -r 2>/dev/null) || LD_KERNEL_RELEASE="" LD_KERNEL_VERSION=$(printf '%s\n' "$LD_KERNEL_RELEASE" | sed -n 's/^\([0-9][0-9]*\.[0-9][0-9]*\(\.[0-9][0-9]*\)\{0,1\}\).*/\1/p') || LD_KERNEL_VERSION="" if [ -z "$LD_KERNEL_VERSION" ]; then LD_KERNEL_STATUS=unknown elif ld_ver_ge "$LD_KERNEL_VERSION" "$LD_KERNEL_MIN"; then LD_KERNEL_STATUS=ok else LD_KERNEL_STATUS=old fi return 0 } # ---------- CPU 与用户空间 ---------- # 读 ELF 头:LD_E_CLASS(1=32 位,2=64 位)、LD_E_MACHINE、LD_E_FLAGS。不是 ELF 时返回 1。 ld_elf() { [ -r "$1" ] || return 1 # shellcheck disable=SC2046 set -- $(dd if="$1" bs=64 count=1 2>/dev/null | od -An -v -tu1 2>/dev/null) [ $# -ge 52 ] || return 1 [ "$1.$2.$3.$4" = "127.69.76.70" ] || return 1 LD_E_CLASS=$5 if [ "$6" != 1 ]; then LD_E_MACHINE=big-endian LD_E_FLAGS=0 return 0 fi LD_E_MACHINE=$(( ${19} + ${20} * 256 )) if [ "$5" = 1 ]; then LD_E_FLAGS=$(( ${37} + ${38} * 256 + ${39} * 65536 + ${40} * 16777216 )) else LD_E_FLAGS=$(( ${49} + ${50} * 256 + ${51} * 65536 + ${52} * 16777216 )) fi return 0 } # 动态链接器路径(PT_INTERP 在文件头一页里)。静态程序没有。 ld_interp() { dd if="$1" bs=4096 count=1 2>/dev/null | tr -c ' -~' '\n' 2>/dev/null \ | sed -n '/^\/[[:print:]]*ld[-.][[:print:]]*\.so/{p;q;}' 2>/dev/null || : } ld_detect_cpu() { LD_MACHINE=$(uname -m 2>/dev/null) || LD_MACHINE="" case $LD_MACHINE in "") LD_KERNEL_BITS="" ;; x86_64|amd64|aarch64|arm64|ppc64*|s390x|riscv64|mips64*) LD_KERNEL_BITS=64 ;; *) LD_KERNEL_BITS=32 ;; esac LD_ARM_LEVEL="" LD_VFP=unknown case $LD_MACHINE in aarch64|arm64|armv8*) LD_ARM_LEVEL=8 ;; armv7*) LD_ARM_LEVEL=7 ;; armv6*) LD_ARM_LEVEL=6 ;; armv5*) LD_ARM_LEVEL=5 ;; esac cpuinfo=$(ld_path /proc/cpuinfo) if [ -r "$cpuinfo" ]; then if [ -z "$LD_ARM_LEVEL" ]; then LD_ARM_LEVEL=$(sed -n 's/^CPU architecture[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$cpuinfo" | head -n 1) || LD_ARM_LEVEL="" fi feats=$(sed -n 's/^Features[[:space:]]*:[[:space:]]*//p' "$cpuinfo" | head -n 1) || feats="" if [ -n "$feats" ]; then case " $feats " in *" vfp "*|*" vfpv3 "*|*" vfpv3d16 "*|*" vfpv4 "*|*" vfpd32 "*|*" fp "*) LD_VFP=yes ;; *) LD_VFP=no ;; esac fi fi # ARMv8 的 AArch32 状态必带 VFP;64 位内核给的是 64 位特性名,可能没有 vfp 字样。 [ "$LD_ARM_LEVEL" = 8 ] && LD_VFP=yes return 0 } # 用户空间按 /bin/sh 这类系统程序的 ELF 头判断,不按 uname -m:64 位内核上可以是 32 位用户空间。 ld_detect_userland() { LD_USER_STATUS=unknown LD_USER_BITS="" LD_USER_MACHINE="" LD_FLOAT="" LD_INTERP="" LD_ELF_FILE="" LD_ARCH="" for f in /bin/sh /bin/busybox /usr/bin/env /bin/ls; do if ld_elf "$(ld_path "$f")"; then LD_ELF_FILE=$f; break; fi done [ -n "$LD_ELF_FILE" ] || return 0 LD_USER_STATUS=ok case $LD_E_CLASS in 1) LD_USER_BITS=32 ;; 2) LD_USER_BITS=64 ;; esac case $LD_E_MACHINE in 62) LD_USER_MACHINE=x86_64 ;; 3) LD_USER_MACHINE=i386 ;; 183) LD_USER_MACHINE=aarch64 ;; 40) LD_USER_MACHINE=arm ;; 8) LD_USER_MACHINE=mips ;; 20) LD_USER_MACHINE=ppc ;; 21) LD_USER_MACHINE=ppc64 ;; 243) LD_USER_MACHINE=riscv ;; 22) LD_USER_MACHINE=s390 ;; *) LD_USER_MACHINE="elf-$LD_E_MACHINE" ;; esac LD_INTERP=$(ld_interp "$(ld_path "$LD_ELF_FILE")") if [ "$LD_USER_MACHINE" = arm ]; then if [ $(( LD_E_FLAGS / 1024 % 2 )) = 1 ]; then LD_FLOAT=hard elif [ $(( LD_E_FLAGS / 512 % 2 )) = 1 ]; then LD_FLOAT=soft else case $LD_INTERP in *armhf*) LD_FLOAT=hard ;; */ld-linux.so.3) LD_FLOAT=soft ;; *) LD_FLOAT=unknown ;; esac fi fi case $LD_USER_MACHINE/$LD_USER_BITS in x86_64/64) LD_ARCH=x86_64 ;; aarch64/64) LD_ARCH=aarch64 ;; i386/32|x86_64/32) LD_ARCH=i686 ;; arm/32) case $LD_ARM_LEVEL in 7|8) LD_ARCH=armv7 ;; 6) LD_ARCH=armv6 ;; 5) LD_ARCH=armv5 ;; *) LD_ARCH=arm ;; esac ;; *) LD_ARCH=$LD_USER_MACHINE ;; esac case $LD_ARCH in aarch64) LD_VFP=yes ;; esac return 0 } # ---------- C 库 ---------- ld_lib_dirs() { for d in /lib /lib64 /usr/lib /usr/lib64; do p=$(ld_path "$d") [ -d "$p" ] && printf '%s\n' "$p" for sub in "$p"/*-linux-gnu*; do [ -d "$sub" ] && printf '%s\n' "$sub"; done done return 0 } ld_detect_libc() { LD_LIBC=unknown LD_LIBC_VERSION="" LD_LIBC_SOURCE="" case $LD_INTERP in *ld-musl*) LD_LIBC=musl LD_LIBC_SOURCE=interpreter; return 0 ;; esac for f in "$(ld_path /lib)"/ld-musl-*; do if [ -e "$f" ]; then LD_LIBC=musl LD_LIBC_SOURCE=loader; return 0; fi done v="" if ld_have getconf; then v=$(getconf GNU_LIBC_VERSION 2>/dev/null | sed -n 's/^glibc[[:space:]]*\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p') || v="" [ -n "$v" ] && LD_LIBC_SOURCE=getconf fi if [ -z "$v" ] && ld_have ldd; then line=$(ldd --version 2>&1 | head -n 1) || line="" case $line in *musl*) LD_LIBC=musl LD_LIBC_SOURCE=ldd; return 0 ;; *GLIBC*|*"GNU libc"*|*"GNU C Library"*) v=$(printf '%s\n' "$line" | sed -n 's/.*[[:space:]]\([0-9][0-9]*\.[0-9][0-9]*\)[^[:space:]]*[[:space:]]*$/\1/p') || v="" [ -n "$v" ] && LD_LIBC_SOURCE=ldd ;; esac fi dirs=$(ld_lib_dirs) if [ -z "$v" ]; then for d in $dirs; do for f in "$d"/libc-[0-9]*.so; do [ -e "$f" ] || continue v=$(printf '%s\n' "${f##*/}" | sed -n 's/^libc-\([0-9][0-9]*\.[0-9][0-9]*\)\.so$/\1/p') || v="" if [ -n "$v" ]; then LD_LIBC_SOURCE=filename; break 2; fi done done fi if [ -z "$v" ]; then for d in $dirs; do [ -r "$d/libc.so.6" ] || continue v=$(tr -c ' -~' '\n' <"$d/libc.so.6" 2>/dev/null \ | sed -n 's/.*release version \([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' | head -n 1) || v="" if [ -n "$v" ]; then LD_LIBC_SOURCE=libc_string; break; fi done fi if [ -n "$v" ]; then LD_LIBC=glibc LD_LIBC_VERSION=$v return 0 fi for d in $dirs; do if [ -e "$d/libc.so.6" ]; then LD_LIBC=glibc LD_LIBC_SOURCE=libc_file; break; fi done return 0 } # ---------- 服务管理、身份、工具 ---------- ld_detect_init() { if [ -d "$(ld_path /run/systemd/system)" ] && ld_have systemctl; then LD_INIT=systemd elif ld_have openrc-run || [ -x "$(ld_path /sbin/openrc-run)" ]; then LD_INIT=openrc else LD_INIT=none fi case $LD_OS_FAMILY/$LD_INIT in dsm/*|qnap/*|*/none) LD_SERVICE=manual ;; *) LD_SERVICE=$LD_INIT ;; esac return 0 } ld_detect_identity() { LD_USER=$(id -un 2>/dev/null) || LD_USER="" LD_UID=$(id -u 2>/dev/null) || LD_UID="" if ld_have sudo; then LD_SUDO=yes; else LD_SUDO=no; fi return 0 } ld_detect_tools() { LD_FETCH="" LD_SHA="" LD_TAR=no LD_GZIP=no LD_BASH=no if ld_have curl; then LD_FETCH=curl; elif ld_have wget; then LD_FETCH=wget; fi if ld_have sha256sum; then LD_SHA=sha256sum elif ld_have busybox && busybox --list 2>/dev/null | grep -qx sha256sum; then LD_SHA=busybox elif ld_have openssl; then LD_SHA=openssl elif ld_have shasum; then LD_SHA=shasum fi ld_have tar && LD_TAR=yes ld_have gzip && LD_GZIP=yes ld_have bash && LD_BASH=yes return 0 } ld_sha256() { case ${LD_SHA:-} in sha256sum) sha256sum "$1" | awk '{print $1}' ;; busybox) busybox sha256sum "$1" | awk '{print $1}' ;; openssl) openssl dgst -sha256 "$1" | awk '{print $NF}' ;; shasum) shasum -a 256 "$1" | awk '{print $1}' ;; *) return 1 ;; esac } ld_fetch() { case ${LD_FETCH:-} in curl) curl -fsSL "$1" -o "$2" ;; wget) wget -q -O "$2" "$1" ;; *) return 1 ;; esac } # ---------- 端口、目录、Docker ---------- ld_detect_port() { LD_PORT=${LD_PORT:-3033} hex=$(printf '%04X' "$LD_PORT") LD_PORT_STATE=unknown for f in "$(ld_path /proc/net/tcp)" "$(ld_path /proc/net/tcp6)"; do [ -r "$f" ] || continue [ "$LD_PORT_STATE" = unknown ] && LD_PORT_STATE=free if awk -v p="$hex" 'NR > 1 && $4 == "0A" { n = split($2, a, ":"); if (toupper(a[n]) == p) f = 1 } END { exit !f }' "$f"; then LD_PORT_STATE=busy break fi done return 0 } ld_free_kb() { df -Pk "$1" 2>/dev/null | awk 'NR == 2 { print $4 }' || : } ld_detect_storage() { LD_VOLUMES="" LD_DIR="" LD_DIR_STATE="" LD_DIR_FREE_KB="" LD_DIR_WRITABLE="" mounts=$(ld_path /proc/mounts) case $LD_OS_FAMILY in dsm) LD_VOLUMES=$(awk '$2 ~ /^\/volume[0-9]+$/ && $3 != "tmpfs" { print $2 }' "$mounts" 2>/dev/null | sort -u | tr '\n' ' ') || LD_VOLUMES="" ;; qnap) LD_VOLUMES=$(awk '$2 ~ /^\/share\/[A-Za-z0-9]+_DATA$/ && $3 != "tmpfs" { print $2 }' "$mounts" 2>/dev/null | sort -u | tr '\n' ' ') || LD_VOLUMES="" ;; esac LD_VOLUMES=${LD_VOLUMES% } case $LD_OS_FAMILY in dsm|qnap) [ -n "$LD_VOLUMES" ] || return 0 LD_DIR="${LD_VOLUMES%% *}/locus" ;; *) LD_DIR=/var/lib/locus ;; esac p=$(ld_path "$LD_DIR") if [ -e "$p" ]; then if [ -e "$p/locus.db" ] || [ -e "$p/app/install.env" ] || [ -e "$p/data/locus.db" ]; then LD_DIR_STATE=locus elif [ -n "$(ls -A "$p" 2>/dev/null | head -n 1)" ]; then LD_DIR_STATE=not_empty else LD_DIR_STATE=empty fi else LD_DIR_STATE=absent fi while [ ! -e "$p" ] && [ "$p" != "${LD_ROOT:-}" ] && [ -n "$p" ]; do p=${p%/*}; done [ -n "$p" ] || p=$(ld_path /) LD_DIR_FREE_KB=$(ld_free_kb "$p") if [ -w "$p" ]; then LD_DIR_WRITABLE=yes; else LD_DIR_WRITABLE=no; fi return 0 } ld_detect_docker() { LD_DOCKER=absent LD_DOCKER_ARCH="" ld_have docker || return 0 # 只问本机的 Docker:远程 context 或 tcp:// 的 DOCKER_HOST 指向别的机器。 case ${DOCKER_HOST:-} in unix://*) dh=$DOCKER_HOST ;; *) dh="" ;; esac if out=$(DOCKER_CONTEXT=default DOCKER_HOST=$dh docker version --format '{{.Server.Arch}}' 2>&1); then LD_DOCKER=ok case $out in amd64|x86_64) LD_DOCKER_ARCH=x86_64 ;; arm64|aarch64) LD_DOCKER_ARCH=aarch64 ;; arm|armv7*) LD_DOCKER_ARCH=armv7 ;; *) LD_DOCKER_ARCH=$(printf '%s' "$out" | head -n 1) ;; esac return 0 fi case $out in *"ermission denied"*) LD_DOCKER=no_permission ;; *"Cannot connect"*|*"ailed to connect"*|*"daemon running"*|*"onnection refused"*|*"o such file"*) LD_DOCKER=daemon_unavailable ;; *) LD_DOCKER=error ;; esac return 0 } ld_docker_has_image() { case " $LD_DOCKER_ARCHES " in *" $1 "*) return 0 ;; esac return 1 } ld_detect() { ld_detect_os ld_detect_kernel ld_detect_cpu ld_detect_userland ld_detect_libc ld_detect_init ld_detect_identity ld_detect_tools ld_detect_port ld_detect_storage ld_detect_docker } # ---------- 选包 ---------- # ld_select TARGETS:选出 LD_PICK(文件名)。选不出时 LD_PICK 为空,LD_REASON 是原因。 ld_select() { LD_PICK="" LD_PICK_LIBC="" LD_PICK_STATUS="" LD_REASON="" LD_NEED="" if [ "$LD_OS_FAMILY" = other ]; then LD_REASON=os_not_linux; return 0; fi if [ "$LD_USER_STATUS" != ok ]; then LD_REASON=userland_unknown; return 0; fi if [ "$LD_KERNEL_STATUS" = unknown ]; then LD_REASON=kernel_unknown; return 0; fi if [ "$LD_KERNEL_STATUS" = old ]; then LD_REASON=kernel_too_old LD_NEED=$LD_KERNEL_MIN; return 0; fi rest=$1 saw=0 reason="" need="" while [ -n "$rest" ]; do line=${rest%%;*} case $rest in *\;*) rest=${rest#*;} ;; *) rest="" ;; esac # shellcheck disable=SC2086 set -- $line [ $# -ge 6 ] || continue [ "$2" = "$LD_ARCH" ] || continue saw=1 if ! ld_ver_ge "$LD_KERNEL_VERSION" "$5"; then reason=kernel_too_old need=$5; continue; fi case $3 in glibc) case $LD_LIBC in glibc) if [ -z "$LD_LIBC_VERSION" ]; then reason=libc_version_unknown; continue; fi if ! ld_ver_ge "$LD_LIBC_VERSION" "$4"; then reason=glibc_too_old need=$4; continue; fi ;; musl) [ -n "$reason" ] || reason=musl_no_package; continue ;; *) [ -n "$reason" ] || reason=libc_unknown; continue ;; esac ;; static) ;; *) continue ;; esac if [ "$2" = armv7 ] && [ "$LD_VFP" != yes ]; then reason=no_vfp; continue; fi LD_PICK=$1 LD_PICK_LIBC=$3 LD_PICK_STATUS=$6 return 0 done if [ "$saw" = 0 ]; then case $LD_ARCH in x86_64|aarch64|armv7) reason=no_package_for_arch ;; *) reason=arch_unsupported ;; esac fi LD_REASON=$reason LD_NEED=$need return 0 } # ld_select_asset TARGETS ASSET:只看清单里名为 ASSET 的那一行。 ld_select_asset() { rows="" rest=$1 while [ -n "$rest" ]; do line=${rest%%;*} case $rest in *\;*) rest=${rest#*;} ;; *) rest="" ;; esac case "$line " in "$2 "*) rows="$rows$line;" ;; esac done if [ -z "$rows" ]; then LD_PICK="" LD_PICK_LIBC="" LD_PICK_STATUS="" LD_REASON=package_missing LD_NEED=$2 return 0 fi ld_select "$rows" } # 已安装的包:服务模式记在 /etc/locus/install.env,手动模式记在 <目录>/app/install.env。 # 0.1.0.000037 及以前的服务安装没有记录,当时只有 GNU 包,按 linux-<架构>.tar.gz 算。 ld_installed_package() { LD_INSTALLED="" # 给了目录就只看那个目录:同一台机器上的服务安装和它无关。 if [ -n "${1:-}" ]; then f="$(ld_path "$1")/app/install.env" [ -r "$f" ] && { LD_INSTALLED=$(sed -n 's/^INSTALLED_PACKAGE=//p' "$f" | head -n 1) || LD_INSTALLED=""; } return 0 fi f=$(ld_path /etc/locus/install.env) [ -r "$f" ] || return 0 LD_INSTALLED=$(sed -n 's/^INSTALLED_PACKAGE=//p' "$f" | head -n 1) || LD_INSTALLED="" [ -n "$LD_INSTALLED" ] && return 0 if [ -x "$(ld_path /usr/local/bin/locus)" ]; then case $LD_ARCH in x86_64|aarch64) LD_INSTALLED="linux-$LD_ARCH.tar.gz" ;; esac fi return 0 } # ld_keep TARGETS:已经装了某个包时,升级只能用同一个包,不换构建变体(musl 不换成 GNU,ARMv7 不换成 ARM64)。 ld_keep() { LD_KEEP_FAILED=0 [ -n "$LD_INSTALLED" ] || return 0 [ "$LD_INSTALLED" = "$LD_PICK" ] && return 0 ld_select_asset "$1" "$LD_INSTALLED" [ -n "$LD_PICK" ] || LD_KEEP_FAILED=1 return 0 } # native / docker / other_host ld_advice() { if [ -n "$LD_PICK" ]; then LD_ADVICE=native elif [ "$LD_DOCKER" = ok ] && ld_docker_has_image "$LD_DOCKER_ARCH"; then LD_ADVICE=docker else LD_ADVICE=other_host fi return 0 } # ---------- 文字 ---------- ld_arch_label() { case $LD_ARCH in x86_64) a=x86_64 ;; aarch64) a="ARM64 (aarch64)" ;; armv7) a=ARMv7 ;; armv6) a=ARMv6 ;; armv5) a=ARMv5 ;; "") a=$(ld_t "未知(uname:${LD_MACHINE:-?})" "unknown (uname: ${LD_MACHINE:-?})") ;; *) a=$LD_ARCH ;; esac [ -n "$LD_USER_BITS" ] && a="$a$(ld_t ",$LD_USER_BITS 位" ", $LD_USER_BITS-bit")" case $LD_FLOAT in hard) a="$a$(ld_t ",硬浮点" ", hard-float")" ;; soft) a="$a$(ld_t ",软浮点" ", soft-float")" ;; unknown) a="$a$(ld_t ",浮点 ABI 未知" ", float ABI unknown")" ;; esac if [ -n "$LD_KERNEL_BITS" ] && [ -n "$LD_USER_BITS" ] && [ "$LD_KERNEL_BITS" != "$LD_USER_BITS" ]; then a="$a$(ld_t "(内核 $LD_KERNEL_BITS 位)" " (kernel $LD_KERNEL_BITS-bit)")" fi printf '%s' "$a" } ld_libc_label() { case $LD_LIBC in glibc) printf 'glibc %s' "${LD_LIBC_VERSION:-$(ld_t "版本未知" "version unknown")}" ;; musl) printf 'musl' ;; *) ld_t "未知" "unknown" ;; esac } ld_reason_text() { if [ "${LD_KEEP_FAILED:-0}" = 1 ]; then ld_t "已安装的是 $LD_INSTALLED,升级不换成别的构建;" "$LD_INSTALLED is installed and upgrades keep that build; " fi case $LD_REASON in package_missing) ld_t "这一版没有 $LD_NEED" "this release has no $LD_NEED" ;; os_not_linux) ld_t "LOCUS 只支持 Linux" "LOCUS runs on Linux only" ;; userland_unknown) ld_t "认不出这台机器的程序格式(读不到系统程序的 ELF 头),不猜测" "cannot tell this machine's program format (no readable ELF header in system programs); not guessing" ;; kernel_unknown) ld_t "读不出内核版本" "cannot read the kernel version" ;; kernel_too_old) ld_t "内核 $LD_KERNEL_VERSION 低于最低要求 $LD_NEED" "kernel $LD_KERNEL_VERSION is older than the minimum $LD_NEED" ;; glibc_too_old) ld_t "glibc $LD_LIBC_VERSION 低于当前发布包要求的 $LD_NEED" "glibc $LD_LIBC_VERSION is older than the $LD_NEED the current package needs" ;; libc_version_unknown) ld_t "有 glibc,但读不出版本" "glibc is present but its version cannot be read" ;; libc_unknown) ld_t "认不出这台机器的 C 库" "cannot identify the C library" ;; musl_no_package) ld_t "这台机器使用 musl,当前发布版本没有可用的包" "this machine uses musl; the current release has no package for it" ;; no_vfp) ld_t "CPU 没有 VFP 浮点单元" "the CPU has no VFP floating-point unit" ;; no_package_for_arch) ld_t "当前发布版本没有 $(ld_arch_label) 的包" "the current release has no package for $(ld_arch_label)" ;; arch_unsupported) ld_t "不支持 $(ld_arch_label)" "$(ld_arch_label) is not supported" ;; *) printf '%s' "$LD_REASON" ;; esac } ld_kb_label() { awk -v k="${1:-0}" 'BEGIN { split("KB MB GB TB", u, " "); i = 1 while (k >= 1024 && i < 4) { k /= 1024; i++ } printf(k < 10 ? "%.1f %s" : "%.0f %s", k, u[i]) }' } ld_docker_text() { case $LD_DOCKER in ok) s="$(ld_t "可用,服务端" "available, server") ${LD_DOCKER_ARCH:-?}" ;; absent) s=$(ld_t "未检测到" "not found") ;; no_permission) s=$(ld_t "已安装,但当前用户没有权限访问(需要 root 或 docker 组)" "installed, but this user may not use it (needs root or the docker group)") ;; daemon_unavailable) s=$(ld_t "有 docker 命令,但 Docker 服务没有运行或连不上" "the docker command exists but the daemon is not running or unreachable") ;; *) s=$(ld_t "有 docker 命令,但查询失败" "the docker command exists but the query failed") ;; esac a=${LD_DOCKER_ARCH:-$LD_ARCH} if [ -n "$a" ] && ! ld_docker_has_image "$a"; then s="$s$(ld_t ";LOCUS 不提供 $a 镜像" "; LOCUS publishes no $a image")" fi printf '%s' "$s" } ld_row() { printf '%s%s\n' "$1" "$2"; } ld_report_text() { sep=$(ld_t ":" ": ") if [ -n "$LD_MODEL" ]; then ld_row "$(ld_t "设备" "Device")$sep" "$LD_VENDOR $LD_MODEL"; fi sys=${LD_OS_NAME:-$(ld_t "未识别的 Linux" "unrecognised Linux")} [ -n "$LD_OS_VERSION" ] && case $sys in *"$LD_OS_VERSION"*) ;; *) sys="$sys $LD_OS_VERSION" ;; esac ld_row "$(ld_t "系统" "System")$sep" "$sys" ld_row "$(ld_t "内核" "Kernel")$sep" "${LD_KERNEL_RELEASE:-?}" ld_row "$(ld_t "架构" "Architecture")$sep" "$(ld_arch_label)" ld_row "$(ld_t "C 库" "C library")$sep" "$(ld_libc_label)" ident="${LD_USER:-?} (UID ${LD_UID:-?})" ld_row "$(ld_t "运行身份" "Running as")$sep" "$ident" case $LD_SERVICE in systemd) svc="systemd" ;; openrc) svc="OpenRC" ;; *) svc=$(ld_t "手动运行,不注册开机启动" "manual; nothing starts on boot") ;; esac ld_row "$(ld_t "服务管理" "Service")$sep" "$svc" tools="" [ -n "$LD_FETCH" ] && tools="$LD_FETCH" || tools=$(ld_t "缺少 curl / wget" "no curl / wget") if [ -n "$LD_SHA" ]; then tools="$tools, sha256: $LD_SHA"; else tools="$tools, $(ld_t "缺少 SHA-256 校验工具" "no SHA-256 tool")"; fi [ "$LD_TAR" = yes ] || tools="$tools, $(ld_t "缺少 tar" "no tar")" [ "$LD_GZIP" = yes ] || tools="$tools, $(ld_t "缺少 gzip" "no gzip")" ld_row "$(ld_t "工具" "Tools")$sep" "$tools" case $LD_PORT_STATE in free) port=$(ld_t "空闲" "free") ;; busy) port=$(ld_t "已被占用" "in use") ;; *) port=$(ld_t "无法检查" "not checked") ;; esac ld_row "$(ld_t "端口 $LD_PORT" "Port $LD_PORT")$sep" "$port" case $LD_OS_FAMILY in dsm|qnap) if [ -n "$LD_VOLUMES" ]; then ld_row "$(ld_t "数据卷" "Volumes")$sep" "$LD_VOLUMES" else ld_row "$(ld_t "数据卷" "Volumes")$sep" "$(ld_t "没有找到" "none found")" fi ;; esac if [ -n "$LD_DIR" ]; then d="$LD_DIR" case $LD_DIR_STATE in locus) d="$d$(ld_t "(已有 LOCUS 数据)" " (has LOCUS data)")" ;; not_empty) d="$d$(ld_t "(已存在且不为空)" " (exists, not empty)")" ;; esac [ -n "$LD_DIR_FREE_KB" ] && d="$d, $(ld_t "可用" "free") $(ld_kb_label "$LD_DIR_FREE_KB")" if [ "$LD_DIR_WRITABLE" = yes ]; then d="$d, $(ld_t "可写" "writable")"; else d="$d, $(ld_t "当前用户不可写" "not writable by this user")"; fi ld_row "$(ld_t "建议目录" "Suggested directory")$sep" "$d" fi if [ -n "$LD_INSTALLED" ]; then ld_row "$(ld_t "已安装" "Installed")$sep" "$LD_INSTALLED"; fi if [ -n "$LD_PICK" ]; then case $LD_PICK_STATUS in verified) st=$(ld_t "已验证" "verified") ;; *) st=$(ld_t "实验性" "experimental") ;; esac ld_row "$(ld_t "原生安装" "Native install")$sep" "$(ld_t "可以,使用 $LD_PICK($st)" "possible with $LD_PICK ($st)")" else ld_row "$(ld_t "原生安装" "Native install")$sep" "$(ld_t "不可以:" "not possible: ")$(ld_reason_text)" fi ld_row "Docker$sep" "$(ld_docker_text)" case $LD_ADVICE in native) case $LD_OS_FAMILY in dsm|qnap) adv=$(ld_t "手动模式:程序和数据放在数据卷上,以 ${LD_USER:-?} 的身份运行,不注册开机启动" "manual mode: program and data on a data volume, running as ${LD_USER:-?}, nothing starts on boot") ;; *) case $LD_SERVICE in manual) adv=$(ld_t "手动模式:以 ${LD_USER:-?} 的身份运行,不注册开机启动" "manual mode: running as ${LD_USER:-?}, nothing starts on boot") ;; *) adv=$(ld_t "可以用安装脚本原生安装" "install natively with the installer") ;; esac ;; esac ;; docker) adv=$(ld_t "原生包不适用,可以用 Docker 镜像运行" "no native package fits; run the Docker image") ;; *) adv=$(ld_t "使用受支持的 Linux 主机部署" "deploy on a supported Linux host") ;; esac ld_row "$(ld_t "建议" "Advice")$sep" "$adv" case $LD_OS_FAMILY in dsm|qnap) if [ "$LD_UID" = 0 ]; then ld_row "" "$(ld_t "注意:当前是 UID 0,手动模式下 LOCUS 将以 root 运行" "Note: this is UID 0; in manual mode LOCUS would run as root")" fi ;; esac return 0 } # ---------- JSON ---------- ld_js() { if [ -z "$1" ]; then printf 'null'; return 0; fi printf '"%s"' "$(printf '%s' "$1" | tr -d '\000-\037' | sed 's/\\/\\\\/g; s/"/\\"/g')" } ld_jn() { if [ -z "$1" ]; then printf 'null'; else printf '%s' "$1"; fi; } ld_jb() { case $1 in yes|true|1) printf 'true' ;; no|false|0) printf 'false' ;; *) printf 'null' ;; esac; } ld_ja() { printf '[' first=1 for item in $1; do [ "$first" = 1 ] || printf ', ' ld_js "$item" first=0 done printf ']' } ld_report_json() { root=no; [ "$LD_UID" = 0 ] && root=yes pick_status=unsupported; [ -n "$LD_PICK" ] && pick_status=supported image=no; ld_docker_has_image "${LD_DOCKER_ARCH:-$LD_ARCH}" && image=yes cat <&2; exit 1; } # ---------- diagnose:只读,不联网 ---------- if [ "${1:-}" = diagnose ]; then shift json=0 dir="" while [ $# -gt 0 ]; do case $1 in --json) json=1 ;; --lang) LD_LANG=${2:-}; [ $# -gt 1 ] && shift ;; --lang=*) LD_LANG=${1#*=} ;; --dir) dir=${2:-}; [ $# -gt 1 ] && shift ;; --dir=*) dir=${1#*=} ;; *) fail "未知参数:$1" "unknown argument: $1" ;; esac shift done case $LD_LANG in zh|en) ;; *) fail "--lang 只接受 zh 或 en" "--lang takes zh or en" ;; esac LD_RELEASE=$ENTRY_VERSION ld_run "${ENTRY_TARGETS:-$LD_LEGACY_TARGETS}" "${dir:-}" if [ "$json" = 1 ]; then ld_report_json; else ld_report_text; fi exit 0 fi # ---------- 安装 ---------- # 从管道执行时没有控制终端,/dev/tty 打不开。有终端才询问语言、把向导的输入接回去。 tty=0 if (: <>/dev/tty) >/dev/null 2>&1; then tty=1; fi if [ "$tty" = 1 ] && [ -z "${LOCUS_LANG:-}" ]; then unattended=0 for arg in "$@"; do case $arg in -y|--yes) unattended=1 ;; esac done if [ "$unattended" = 0 ]; then printf '\n 1) 中文\n 2) English\n 语言 / Language [1]: ' >/dev/tty IFS= read -r answer /dev/null; then targets=$(grep -v '^#' "$work/targets" | tr '\n' ';') else targets=$LD_LEGACY_TARGETS fi # 已有的手动安装在 --dir 指定的目录里,要按它装的构建变体选包。 dir="" prev="" for arg in "$@"; do case $prev in --dir) dir=$arg ;; esac case $arg in --dir=*) dir=${arg#*=} ;; esac prev=$arg done LD_RELEASE=$VERSION ld_run "$targets" "$dir" if [ -z "$LD_PICK" ]; then ld_report_text >&2 fail "LOCUS $VERSION 不能在这台机器上原生安装:$(ld_reason_text)。没有下载任何东西。" \ "LOCUS $VERSION cannot be installed natively here: $(ld_reason_text). Nothing was downloaded." fi [ "$LD_BASH" = yes ] || fail "安装器需要 bash" "the installer needs bash" if [ "$LD_PICK_STATUS" != verified ]; then say "注意:$LD_PICK 是实验性的包,还没有在这类机器上实测过。" \ "Note: $LD_PICK is experimental and has not been tested on this kind of machine." fi ld_fetch "$BASE/$VERSION/SHA256SUMS" "$work/SHA256SUMS" || fail "下载 SHA256SUMS 失败" "cannot download SHA256SUMS" # fetch_checked 文件名:下载到 $work 并按 SHA256SUMS 校验,不在清单里或对不上就停。 fetch_checked() { want=$(awk -v f="$1" '$2 == f || $2 == "*" f { print $1; exit }' "$work/SHA256SUMS") [ -n "$want" ] || fail "SHA256SUMS 里没有 $1" "SHA256SUMS has no $1" ld_fetch "$BASE/$VERSION/$1" "$work/$1" || fail "下载 $1 失败" "cannot download $1" [ "$(ld_sha256 "$work/$1")" = "$want" ] || fail "$1 校验失败,请重新下载" "$1 checksum mismatch, download again" } # 新版本:只下载这台机器要的那一个包和不带载荷的安装器。 # 较早的版本只有内嵌全部包的 linux-install.sh,也只有 GNU 包,不能用在 NAS 和 musl 变体上。 if awk '$2 == "linux-setup.sh" || $2 == "*linux-setup.sh" { found = 1 } END { exit !found }' "$work/SHA256SUMS"; then fetch_checked "$LD_PICK" fetch_checked linux-setup.sh setup=$work/linux-setup.sh set -- "$@" --package "$work/$LD_PICK" else case $LD_OS_FAMILY/$LD_PICK in dsm/*|qnap/*|*/*-musl*) fail "LOCUS $VERSION 还不支持这台机器($LD_PICK);请安装更新的版本。没有安装任何东西。" \ "LOCUS $VERSION does not support this machine yet ($LD_PICK); install a newer release. Nothing was installed." ;; esac fetch_checked linux-install.sh setup=$work/linux-install.sh fi chmod 755 "$setup" # 安装器会用 sudo 重新执行同一个路径、读同一个包,这里不能在退出时删掉。 trap - EXIT LOCUS_LANG=$LD_LANG # 较早的安装器只看 LC_ALL / LC_MESSAGES / LANG,sudo 重新运行时也会保留这些变量。 case $LOCUS_LANG in zh) unset LC_ALL; LC_MESSAGES=zh_CN.UTF-8 ;; en) unset LC_ALL; LC_MESSAGES=C.UTF-8 ;; esac export LOCUS_LANG LC_MESSAGES if [ "$tty" = 1 ]; then exec bash "$setup" "$@"