Split Buildroot flow into dedicated manager

This commit is contained in:
Yellowcooln
2026-04-22 22:26:18 -04:00
parent 7cbaa9115e
commit 34fe07d7b0
2 changed files with 508 additions and 144 deletions

461
buildroot-manage.sh Normal file
View File

@@ -0,0 +1,461 @@
#!/bin/bash
# Buildroot/Luckfox management entrypoint for pyMC Repeater
set -euo pipefail
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
INSTALL_DIR="/opt/pymc_repeater"
VENV_DIR="$INSTALL_DIR/venv"
VENV_PIP="$VENV_DIR/bin/pip"
VENV_PYTHON="$VENV_DIR/bin/python"
CONFIG_DIR="/etc/pymc_repeater"
LOG_DIR="/var/log/pymc_repeater"
DATA_DIR="/var/lib/pymc_repeater"
SERVICE_USER="repeater"
INIT_SCRIPT="/etc/init.d/S80pymc-repeater"
PIDFILE="/var/run/pymc-repeater.pid"
LOGFILE="$LOG_DIR/repeater.log"
SERVICE_NAME="pymc-repeater"
SILENT_MODE="${PYMC_SILENT:-${SILENT:-}}"
R2_BASE_URL="https://wheel.pymc.dev/pymc_build_deps"
R2_ENABLED=1
stage() {
printf '\n==> %s\n' "$1"
}
info() {
printf ' - %s\n' "$1"
}
warn() {
printf ' - %s\n' "$1" >&2
}
fail() {
printf '%s\n' "$1" >&2
exit 1
}
need_cmd() {
command -v "$1" >/dev/null 2>&1 || fail "Missing required command: $1"
}
is_buildroot() {
[ -f /etc/pymc-image-build-id ] && return 0
[ -f /etc/os-release ] && grep -q '^ID=buildroot$' /etc/os-release 2>/dev/null && return 0
return 1
}
ensure_root() {
[ "${EUID}" -eq 0 ] || fail "This command must be run as root."
}
group_exists() {
grep -q "^$1:" /etc/group 2>/dev/null
}
ensure_group_line() {
local group_name="$1"
local gid="$2"
group_exists "$group_name" && return 0
printf '%s:x:%s:\n' "$group_name" "$gid" >> /etc/group
}
ensure_service_user() {
if id "$SERVICE_USER" >/dev/null 2>&1; then
return 0
fi
if command -v useradd >/dev/null 2>&1; then
useradd --system --home "$DATA_DIR" --shell /sbin/nologin "$SERVICE_USER"
return 0
fi
ensure_group_line "$SERVICE_USER" 990
printf '%s:x:990:990::%s:/sbin/nologin\n' "$SERVICE_USER" "$DATA_DIR" >> /etc/passwd
if [ -f /etc/shadow ]; then
printf '%s:!:19000:0:99999:7:::\n' "$SERVICE_USER" >> /etc/shadow
fi
}
add_user_to_group() {
local user_name="$1"
local group_name="$2"
local current_line current_members gid escaped_line new_members
group_exists "$group_name" || return 0
current_line=$(grep "^${group_name}:" /etc/group 2>/dev/null || true)
[ -n "$current_line" ] || return 0
current_members=$(printf '%s' "$current_line" | cut -d: -f4)
case ",${current_members}," in
*,"${user_name}",*) return 0 ;;
esac
if [ -n "$current_members" ]; then
new_members="${current_members},${user_name}"
else
new_members="${user_name}"
fi
gid=$(printf '%s' "$current_line" | cut -d: -f3)
escaped_line=$(printf '%s\n' "$current_line" | sed 's/[].[^$\\*]/\\&/g')
sed -i "s/^${escaped_line}\$/${group_name}:x:${gid}:${new_members}/" /etc/group
}
install_system_packages() {
if is_buildroot; then
info "Buildroot image detected; using preinstalled packages."
return 0
fi
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y \
libffi-dev libusb-1.0-0 sudo jq pip python3-venv python3-rrdtool wget swig build-essential python3-dev
}
ensure_yq() {
local yq_version="v4.40.5"
local yq_binary="yq_linux_arm64"
if command -v yq >/dev/null 2>&1 && yq --version 2>&1 | grep -q 'mikefarah/yq'; then
return 0
fi
case "$(uname -m)" in
x86_64) yq_binary="yq_linux_amd64" ;;
armv7*|armv6*|armhf) yq_binary="yq_linux_arm" ;;
aarch64|arm64) yq_binary="yq_linux_arm64" ;;
esac
wget -qO /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${yq_version}/${yq_binary}"
chmod +x /usr/local/bin/yq
}
ensure_venv() {
if [ ! -x "$VENV_PYTHON" ]; then
stage "Creating virtual environment"
python3 -m venv --system-site-packages "$VENV_DIR"
"$VENV_PIP" install --upgrade pip setuptools wheel >/dev/null 2>&1 || true
fi
}
preinstall_r2_wheels() {
local machine_arch arch_tag platform_tag py_tag wheel_base
[ "$R2_ENABLED" -eq 1 ] || return 0
machine_arch=$(uname -m)
case "$machine_arch" in
aarch64) arch_tag="arm64"; platform_tag="aarch64" ;;
armv7l|armv7) arch_tag="armv7"; platform_tag="armv7l" ;;
x86_64) arch_tag="x86_64"; platform_tag="x86_64" ;;
*) return 0 ;;
esac
py_tag=$("$VENV_PYTHON" -c 'import sys; v=f"cp{sys.version_info.major}{sys.version_info.minor}"; print(f"{v}-{v}")' 2>/dev/null || echo "cp311-cp311")
wheel_base="${R2_BASE_URL}/${arch_tag}/${platform_tag}/${py_tag}"
"$VENV_PIP" install --find-links "${wheel_base}/index.html" --no-cache-dir \
"pycryptodome>=3.23.0" "PyNaCl>=1.5.0" cffi "pyyaml>=6.0.0" >/dev/null 2>&1 || true
}
create_init_script() {
cat > "$INIT_SCRIPT" <<EOF
#!/bin/sh
DAEMON="${VENV_PYTHON}"
PIDFILE="${PIDFILE}"
LOGFILE="${LOGFILE}"
WORKDIR="${DATA_DIR}"
CONFIG_FILE="${CONFIG_DIR}/config.yaml"
RUN_AS="${SERVICE_USER}"
start() {
mkdir -p "$(dirname "\$PIDFILE")" "$(dirname "\$LOGFILE")" "\$WORKDIR"
if [ -f "\$PIDFILE" ] && kill -0 "$(cat "\$PIDFILE")" 2>/dev/null; then
echo "${SERVICE_NAME} is already running."
return 0
fi
start-stop-daemon --start --quiet --background --make-pidfile --pidfile "\$PIDFILE" \\
--chuid "\$RUN_AS" --exec /bin/sh -- -c "cd \"\$WORKDIR\" && exec \"\$DAEMON\" -m repeater.main --config \"\$CONFIG_FILE\" >>\"\$LOGFILE\" 2>&1"
}
stop() {
if [ ! -f "\$PIDFILE" ]; then
echo "${SERVICE_NAME} is not running."
return 0
fi
start-stop-daemon --stop --quiet --retry 5 --pidfile "\$PIDFILE" || true
rm -f "\$PIDFILE"
}
status() {
if [ -f "\$PIDFILE" ] && kill -0 "$(cat "\$PIDFILE")" 2>/dev/null; then
echo "${SERVICE_NAME} is running."
return 0
fi
echo "${SERVICE_NAME} is stopped."
return 1
}
case "\${1:-}" in
start) start ;;
stop) stop ;;
restart) stop; start ;;
status) status ;;
*)
echo "Usage: \$0 {start|stop|restart|status}"
exit 1
;;
esac
EOF
chmod 0755 "$INIT_SCRIPT"
}
service_exists() {
[ -x "$INIT_SCRIPT" ]
}
is_installed() {
[ -d "$INSTALL_DIR" ] && service_exists
}
is_running() {
[ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null
}
get_version() {
if [ -x "$VENV_PYTHON" ]; then
"$VENV_PYTHON" -c "from importlib.metadata import version; print(version('pymc_repeater'))" 2>/dev/null || echo "not installed"
else
echo "not installed"
fi
}
doctor() {
stage "Checking Buildroot image baseline"
for cmd in bash git python3 dialog jq wget sudo sqlite3 start-stop-daemon; do
if command -v "$cmd" >/dev/null 2>&1; then
info "found $cmd"
else
warn "missing $cmd"
fi
done
if python3 -m venv --help >/dev/null 2>&1; then
info "python venv support available"
else
warn "python venv support missing"
fi
if python3 - <<'PY'
modules = [
"sqlite3",
"yaml",
"cherrypy",
"cherrypy_cors",
"autocommand",
"jaraco.collections",
"jaraco.text",
"paho.mqtt.client",
"psutil",
"jwt",
"ws4py",
"nacl",
"periphery",
"spidev",
"serial",
"usb",
"Crypto",
]
for module in modules:
__import__(module)
PY
then
info "python runtime packages are present"
else
warn "python runtime packages are missing"
fi
for path in /dev/spidev* /dev/gpiochip*; do
[ -e "$path" ] && info "detected $path"
done
}
install_repeater() {
local git_version machine_arch arch_tag platform_tag py_tag wheel_base ip_address
ensure_root
stage "Preparing Buildroot installation"
install_system_packages
ensure_service_user
for grp in plugdev dialout gpio i2c spi; do
add_user_to_group "$SERVICE_USER" "$grp"
done
mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR" "$DATA_DIR/.config/pymc_repeater"
chown -R "$SERVICE_USER:$SERVICE_USER" "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR"
chmod 755 "$INSTALL_DIR" "$DATA_DIR"
chmod 750 "$CONFIG_DIR" "$LOG_DIR"
cp "$SCRIPT_DIR/config.yaml.example" "$CONFIG_DIR/config.yaml.example"
[ -f "$CONFIG_DIR/config.yaml" ] || cp "$SCRIPT_DIR/config.yaml.example" "$CONFIG_DIR/config.yaml"
cp "$SCRIPT_DIR/radio-settings.json" "$DATA_DIR/" 2>/dev/null || true
cp "$SCRIPT_DIR/radio-presets.json" "$DATA_DIR/" 2>/dev/null || true
ensure_yq
ensure_venv
if [ -d "$SCRIPT_DIR/.git" ]; then
git -C "$SCRIPT_DIR" fetch --tags 2>/dev/null || true
git_version=$(python3 -m setuptools_scm 2>/dev/null || echo "1.0.5")
export SETUPTOOLS_SCM_PRETEND_VERSION="$git_version"
else
export SETUPTOOLS_SCM_PRETEND_VERSION="1.0.5"
fi
if ! grep -q "Luckfox Pico" /proc/device-tree/model 2>/dev/null; then
export PIP_ONLY_BINARY=pycryptodome,cffi,PyNaCl,psutil
fi
preinstall_r2_wheels
stage "Installing pyMC Repeater into venv"
(cd "$SCRIPT_DIR" && "$VENV_PIP" install --upgrade --no-cache-dir .[hardware])
create_init_script
stage "Starting service"
"$INIT_SCRIPT" restart
ip_address=$(hostname -I | awk '{print $1}')
if is_running; then
printf '\nService is running on: http://%s:8000\n' "${ip_address}"
else
fail "Installation completed but the service failed to start. Check: sh $0 logs"
fi
}
upgrade_repeater() {
ensure_root
is_installed || fail "Service is not installed."
ensure_venv
preinstall_r2_wheels
stage "Upgrading pyMC Repeater"
(cd "$SCRIPT_DIR" && "$VENV_PIP" install --upgrade --no-cache-dir .[hardware])
"$INIT_SCRIPT" restart
}
uninstall_repeater() {
ensure_root
stage "Removing service"
service_exists && "$INIT_SCRIPT" stop || true
rm -f "$INIT_SCRIPT"
rm -rf "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" "$DATA_DIR"
}
manage_service() {
local action="$1"
ensure_root
service_exists || fail "Service is not installed."
"$INIT_SCRIPT" "$action"
}
show_status() {
local ip_address version
version=$(get_version)
ip_address=$(hostname -I | awk '{print $1}')
if ! is_installed; then
printf 'Installation Status: Not Installed\n'
return 0
fi
printf 'Installation Status: Installed\n'
printf 'Version: %s\n' "$version"
printf 'Install Directory: %s\n' "$INSTALL_DIR"
printf 'Config Directory: %s\n' "$CONFIG_DIR"
printf 'Log File: %s\n' "$LOGFILE"
printf 'Dashboard: http://%s:8000\n' "$ip_address"
if is_running; then
printf 'Service Status: Running\n'
else
printf 'Service Status: Stopped\n'
fi
}
show_logs() {
mkdir -p "$LOG_DIR"
touch "$LOGFILE"
tail -f "$LOGFILE"
}
run_debug() {
ensure_root
mkdir -p "$LOG_DIR" "$DATA_DIR"
exec "$VENV_PYTHON" -m repeater.main --config "$CONFIG_DIR/config.yaml"
}
delegate_to_stock_manage() {
exec bash "$SCRIPT_DIR/manage.sh" "$@"
}
usage() {
cat <<'EOF'
Usage: bash buildroot-manage.sh <command>
Commands:
doctor Check Buildroot/Luckfox prerequisites
install Install pyMC Repeater on the Buildroot image
upgrade Upgrade the Buildroot installation from the checked-out repo
config Run the stock interactive config flow
start Start the init.d service
stop Stop the init.d service
restart Restart the init.d service
status Show Buildroot service status
logs Tail the Buildroot log file
uninstall Remove the Buildroot installation
debug Run repeater.main in the foreground
EOF
}
case "${1:-}" in
doctor)
doctor
;;
install)
install_repeater
;;
upgrade)
upgrade_repeater
;;
config)
shift
delegate_to_stock_manage config "$@"
;;
start|stop|restart)
manage_service "$1"
;;
status)
show_status
;;
logs)
show_logs
;;
uninstall)
uninstall_repeater
;;
debug)
run_debug
;;
""|help|-h|--help)
usage
;;
*)
fail "Unknown command: ${1}"
;;
esac

191
manage.sh
View File

@@ -17,134 +17,6 @@ SILENT_MODE="${PYMC_SILENT:-${SILENT:-}}"
R2_BASE_URL="https://wheel.pymc.dev/pymc_build_deps"
R2_ENABLED=1 # Set to 0 to disable R2 wheels and always build from source
# ---------------------------------------------------------------------------
# Platform detection / compatibility helpers
# ---------------------------------------------------------------------------
is_buildroot() {
[ -f /etc/pymc-image-build-id ] && return 0
[ -f /etc/os-release ] && grep -q '^ID=buildroot$' /etc/os-release 2>/dev/null && return 0
return 1
}
group_exists() {
local group_name="$1"
if command -v getent >/dev/null 2>&1; then
getent group "$group_name" >/dev/null 2>&1
else
grep -q "^${group_name}:" /etc/group 2>/dev/null
fi
}
ensure_group_line() {
local group_name="$1"
local gid="$2"
group_exists "$group_name" && return 0
printf '%s:x:%s:\n' "$group_name" "$gid" >> /etc/group
}
ensure_service_user() {
if id "$SERVICE_USER" >/dev/null 2>&1; then
return 0
fi
if command -v useradd >/dev/null 2>&1; then
useradd --system --home /var/lib/pymc_repeater --shell /sbin/nologin "$SERVICE_USER"
return 0
fi
if is_buildroot; then
ensure_group_line "$SERVICE_USER" 990
printf '%s:x:990:990::/var/lib/pymc_repeater:/sbin/nologin\n' "$SERVICE_USER" >> /etc/passwd
if [ -f /etc/shadow ]; then
printf '%s:!:19000:0:99999:7:::\n' "$SERVICE_USER" >> /etc/shadow
fi
return 0
fi
echo "Error: useradd is not available on this system." >&2
exit 1
}
add_user_to_group() {
local user_name="$1"
local group_name="$2"
group_exists "$group_name" || return 0
if command -v usermod >/dev/null 2>&1; then
usermod -a -G "$group_name" "$user_name" 2>/dev/null || true
return 0
fi
if is_buildroot; then
local current_line current_members gid new_members escaped_line
current_line=$(grep "^${group_name}:" /etc/group 2>/dev/null || true)
[ -n "$current_line" ] || return 0
current_members=$(printf '%s' "$current_line" | cut -d: -f4)
case ",${current_members}," in
*,"${user_name}",*) return 0 ;;
esac
if [ -n "$current_members" ]; then
new_members="${current_members},${user_name}"
else
new_members="${user_name}"
fi
gid=$(printf '%s' "$current_line" | cut -d: -f3)
escaped_line=$(printf '%s\n' "$current_line" | sed 's/[].[^$\\*]/\\&/g')
sed -i "s/^${escaped_line}\$/${group_name}:x:${gid}:${new_members}/" /etc/group
fi
}
install_system_packages() {
if is_buildroot; then
echo " Buildroot image detected; using preinstalled system packages."
return 0
fi
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y \
libffi-dev libusb-1.0-0 sudo jq pip python3-venv python3-rrdtool wget swig build-essential python3-dev
DEBIAN_FRONTEND=noninteractive apt-get install -y policykit-1 2>/dev/null \
|| DEBIAN_FRONTEND=noninteractive apt-get install -y polkitd pkexec 2>/dev/null \
|| echo " Warning: Could not install polkit (sudo fallback will be used)"
}
install_bootstrap_python_packages() {
if is_buildroot; then
return 0
fi
pip install --break-system-packages setuptools_scm >/dev/null 2>&1 \
|| python3 -m pip install --break-system-packages setuptools_scm >/dev/null 2>&1 \
|| true
}
ensure_yq() {
if command -v yq &>/dev/null && [[ "$(yq --version 2>&1)" == *"mikefarah/yq"* ]]; then
return 0
fi
echo ">>> Installing yq..."
local yq_version="v4.40.5"
local yq_binary="yq_linux_arm64"
case "$(uname -m)" in
x86_64)
yq_binary="yq_linux_amd64"
;;
armv7*|armv6*|armhf)
yq_binary="yq_linux_arm"
;;
aarch64|arm64)
yq_binary="yq_linux_arm64"
;;
esac
wget -qO /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${yq_version}/${yq_binary}" 2>/dev/null \
&& chmod +x /usr/local/bin/yq
}
# ---------------------------------------------------------------------------
# Virtual-environment helpers
# ---------------------------------------------------------------------------
@@ -231,18 +103,13 @@ elif command -v dialog &> /dev/null; then
DIALOG="dialog"
else
echo "TUI interface requires whiptail or dialog."
if [ "$EUID" -eq 0 ] && ! is_buildroot; then
if [ "$EUID" -eq 0 ]; then
echo "Installing whiptail..."
apt-get update -qq && apt-get install -y whiptail
DIALOG="whiptail"
else
echo ""
if is_buildroot; then
echo "This Buildroot image is expected to ship with dialog preinstalled."
echo "Please rebuild the image with dialog enabled, or run from a system that has dialog."
else
echo "Please install whiptail: sudo apt-get install -y whiptail"
fi
echo "Please install whiptail: sudo apt-get install -y whiptail"
echo "Then run this script again."
exit 1
fi
@@ -459,25 +326,44 @@ install_repeater() {
echo ""
echo ">>> Creating service user..."
ensure_service_user
if ! id "$SERVICE_USER" &>/dev/null; then
useradd --system --home /var/lib/pymc_repeater --shell /sbin/nologin "$SERVICE_USER"
fi
(
echo "10"; echo "# Adding user to hardware groups..."
for grp in plugdev dialout gpio i2c spi; do
add_user_to_group "$SERVICE_USER" "$grp"
getent group "$grp" >/dev/null 2>&1 && usermod -a -G "$grp" "$SERVICE_USER" 2>/dev/null || true
done
echo "20"; echo "# Creating directories..."
mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" /var/lib/pymc_repeater
echo "25"; echo "# Installing system dependencies..."
install_system_packages
install_bootstrap_python_packages
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y libffi-dev libusb-1.0-0 sudo jq pip python3-venv python3-rrdtool wget swig build-essential python3-dev
# Install polkit (package name varies by distro version)
DEBIAN_FRONTEND=noninteractive apt-get install -y policykit-1 2>/dev/null \
|| DEBIAN_FRONTEND=noninteractive apt-get install -y polkitd pkexec 2>/dev/null \
|| echo " Warning: Could not install polkit (sudo fallback will be used)"
# setuptools_scm needed for git version detection during build
pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || python3 -m pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || true
echo "28"; echo "# Creating virtual environment..."
ensure_venv
ensure_yq
# Install mikefarah yq v4 if not already installed
if ! command -v yq &> /dev/null || [[ "$(yq --version 2>&1)" != *"mikefarah/yq"* ]]; then
echo ">>> Installing yq..."
YQ_VERSION="v4.40.5"
YQ_BINARY="yq_linux_arm64"
if [[ "$(uname -m)" == "x86_64" ]]; then
YQ_BINARY="yq_linux_amd64"
elif [[ "$(uname -m)" == "armv7"* ]]; then
YQ_BINARY="yq_linux_arm"
fi
wget -qO /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${YQ_BINARY}" 2>/dev/null && chmod +x /usr/local/bin/yq
fi
echo "29"; echo "# Installing files..."
cp "$SCRIPT_DIR/manage.sh" "$INSTALL_DIR/" 2>/dev/null || true
@@ -858,9 +744,26 @@ upgrade_repeater() {
fi
echo "[3/9] Updating system dependencies..."
install_system_packages
install_bootstrap_python_packages
ensure_yq
apt-get update -qq
apt-get install -y libffi-dev libusb-1.0-0 sudo jq pip python3-venv python3-rrdtool wget swig build-essential python3-dev
# Install polkit (package name varies by distro version)
apt-get install -y policykit-1 2>/dev/null \
|| apt-get install -y polkitd pkexec 2>/dev/null \
|| echo " Warning: Could not install polkit (sudo fallback will be used)"
pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || python3 -m pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || true
# Install mikefarah yq v4 if not already installed
if ! command -v yq &> /dev/null || [[ "$(yq --version 2>&1)" != *"mikefarah/yq"* ]]; then
YQ_VERSION="v4.40.5"
YQ_BINARY="yq_linux_arm64"
if [[ "$(uname -m)" == "x86_64" ]]; then
YQ_BINARY="yq_linux_amd64"
elif [[ "$(uname -m)" == "armv7"* ]]; then
YQ_BINARY="yq_linux_arm"
fi
wget -qO /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${YQ_BINARY}" && chmod +x /usr/local/bin/yq
fi
echo " ✓ Dependencies updated"
echo "[4/9] Installing files..."
@@ -881,7 +784,7 @@ upgrade_repeater() {
echo "[5.5/9] Ensuring user groups and udev rules..."
for grp in plugdev dialout gpio i2c spi; do
add_user_to_group "$SERVICE_USER" "$grp"
getent group "$grp" >/dev/null 2>&1 && usermod -a -G "$grp" "$SERVICE_USER" 2>/dev/null || true
done
# Install/update CH341 udev rules
SCRIPT_DIR_UPGRADE="$(cd "$(dirname "$0")" && pwd)"