Files
radar_system/deploy/update.sh
T

190 lines
8.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# Install a release archive made by deploy/pack-usb.sh over the project on this
# device. The code is replaced; configs, calibrations and data are kept; the current
# version stays next to it as radar_system<N> (the next free number) for a rollback.
#
# bash update.sh [--python VERSION|PATH] [ARCHIVE]
#
# ARCHIVE the newest radar_system-*.tar.gz next to this script by default
# --python build a fresh venv with this interpreter, e.g. 3.13 (found with uv or as
# python3.13); without it the current venv is carried over
# The project is radar_system in the Documents folder unless RADAR_DIR names it.
set -Eeuo pipefail
shopt -s nullglob
say() { echo "[update] $*"; }
fail() { echo "[update] $*" >&2; exit 1; }
(( EUID != 0 )) || fail "Run it as your usual user, not with sudo: the project files must stay yours."
if [[ -n "${RADAR_DIR:-}" ]]; then
target="${RADAR_DIR}"
else
target="${HOME}/Documents/radar_system"
localized="$(xdg-user-dir DOCUMENTS 2>/dev/null || true)/radar_system" # e.g. ~/Документы
if [[ ! -e "${target}" && -d "${localized}" ]]; then target="${localized}"; fi
fi
[[ ! -L "${target}" ]] || fail "${target} is a symlink; set RADAR_DIR to the real directory."
target="$(realpath -m -- "${target}")"
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
python_request=""
archive=""
while (( $# > 0 )); do
case "$1" in
--python) python_request="${2:?--python needs a version or a path}"; shift 2 ;;
-h|--help) sed -n '2,12p' "${BASH_SOURCE[0]}"; exit 0 ;;
*) archive="$1"; shift ;;
esac
done
if [[ -n "${archive}" ]]; then archive="$(realpath -- "${archive}")"; fi
cd "${HOME}" # never stand in the directory that is about to be renamed
# --- check everything before touching the project ------------------------------
if [[ -z "${archive}" ]]; then
releases=("${script_dir}"/radar_system-*.tar.gz)
(( ${#releases[@]} > 0 )) || fail "No radar_system-*.tar.gz next to ${BASH_SOURCE[0]}."
archive="${releases[-1]}" # the names sort by commit date
fi
listing="$(tar -tzf "${archive}")" || fail "Damaged archive: ${archive}"
grep -qx 'radar_system/start.sh' <<< "${listing}" || fail "Not a radar_system release: ${archive}"
if [[ ! -e "${target}" ]]; then
read -r -p "[update] No project at ${target}. Install a fresh one there? [y/N] " answer
[[ "${answer}" == [yY]* ]] || exit 1
fi
python=""
if [[ -n "${python_request}" ]]; then
if [[ "${python_request}" =~ ^3\.[0-9]+$ ]]; then
uv="$(command -v uv || echo "${HOME}/.local/bin/uv")"
if [[ -x "${uv}" ]]; then
python="$("${uv}" python find "${python_request}" 2>/dev/null || true)"
fi
[[ -n "${python}" ]] || python="$(command -v "python${python_request}" || true)"
[[ -n "${python}" ]] || fail "Python ${python_request} not found; install it: uv python install ${python_request}"
else
python="$(command -v -- "${python_request}" || true)"
[[ -n "${python}" ]] || fail "Interpreter not found: ${python_request}"
fi
say "venv interpreter: ${python} ($("${python}" --version 2>&1))"
fi
parent="$(dirname -- "${target}")"
mkdir -p -- "${parent}"
staging="$(mktemp -d "${parent}/.radar_update.XXXXXX")"
trap 'rm -rf -- "${staging}"' EXIT
tar -xzf "${archive}" -C "${staging}"
# --- stop everything that runs from the project ----------------------------------
daemon_stopped=0
if command -v systemctl >/dev/null && systemctl is-active --quiet radar.service; then
say "stopping radar.service"
sudo systemctl stop radar.service
daemon_stopped=1
fi
start_daemon() {
if (( daemon_stopped )); then
say "starting radar.service"
sudo systemctl start radar.service
fi
}
busy=()
for proc in /proc/[0-9]*; do
pid="${proc#/proc/}"
[[ "${pid}" == "$$" || "${pid}" == "${PPID}" ]] && continue
cwd="$(readlink "${proc}/cwd" 2>/dev/null)" || continue
[[ "${cwd}" == "${target}" || "${cwd}" == "${target}/"* ]] && busy+=("${pid}")
done
if (( ${#busy[@]} > 0 )); then
echo "[update] The program still runs from ${target}; close it and run the update again:" >&2
ps -o pid=,args= -p "$(IFS=,; echo "${busy[*]}")" >&2 || true
start_daemon
exit 1
fi
# --- swap the versions --------------------------------------------------------------
backup=""
if [[ -e "${target}" ]]; then
n=1
while [[ -e "${target}${n}" ]]; do n=$((n + 1)); done
backup="${target}${n}"
mv -- "${target}" "${backup}"
on_failure() {
echo "[update] FAILED. The previous version is ${backup}; to go back to it:" >&2
echo " rm -rf '${target}' && mv '${backup}' '${target}'" >&2
if (( daemon_stopped )); then echo " sudo systemctl start radar.service" >&2; fi
}
trap on_failure ERR
fi
mv -- "${staging}/radar_system" "${target}"
say "installed ${archive##*/} into ${target}"
# --- keep this device's state ----------------------------------------------------------
if [[ -n "${backup}" ]]; then
say "previous version: ${backup}"
kept=()
# The active profile; the release carries the development one (mock driver).
if [[ -f "${backup}/run_config.json" ]]; then
cp -a -- "${backup}/run_config.json" "${target}/run_config.json"
kept+=(run_config.json)
fi
# Profiles changed or added on this device win; a changed one gets the release
# version next to it as .new.
renamed=()
for profile in "${backup}"/run_config_examples/*.json; do
shipped="${target}/run_config_examples/${profile##*/}"
if [[ -f "${shipped}" ]]; then
cmp -s -- "${profile}" "${shipped}" && continue
mv -- "${shipped}" "${shipped}.new"
renamed+=("${profile##*/}")
fi
cp -a -- "${profile}" "${shipped}"
kept+=("run_config_examples/${profile##*/}")
done
# Runtime state (last profile, live settings), without logs and pid files.
if [[ -d "${backup}/python_app/runtime" ]]; then
cp -a -- "${backup}/python_app/runtime" "${target}/python_app/runtime"
rm -rf -- "${target}/python_app/runtime/logs" "${target}/python_app/runtime/"*.pids
kept+=(python_app/runtime)
fi
# Data: calibrations, references, snapshots, and every top-level folder that is
# not part of the release. Hard links are instant and take no space; the program
# writes new files instead of rewriting old ones.
data=()
for entry in "${backup}"/python_app/data*; do data+=("python_app/${entry##*/}"); done
for entry in "${backup}"/*; do
name="${entry##*/}"
case "${name}" in build|graphify-out|device_firmware|docs|RELEASE) continue ;; esac
[[ -e "${target}/${name}" ]] || data+=("${name}")
done
for entry in "${data[@]}"; do
cp -al -- "${backup}/${entry}" "${target}/${entry}"
kept+=("${entry}")
done
say "kept from ${backup##*/}: ${kept[*]:-nothing}"
if (( ${#renamed[@]} > 0 )); then
say "profiles changed on this device are kept, the release ones are next to them as .new: ${renamed[*]}"
fi
fi
# --- Python environment and C++ build ------------------------------------------------
venv="${target}/.venv"
if [[ -z "${python}" && -n "${backup}" && -x "${backup}/.venv/bin/python" ]]; then
say "carrying the venv over"
# pip and generated caches may rewrite existing files: the rollback copy
# must own its files independently of the environment being upgraded.
cp -a -- "${backup}/.venv" "${venv}"
"${venv}/bin/python" -m pip install --quiet -r "${target}/requirements.txt"
else
python="${python:-$(command -v python3)}"
say "creating a venv with ${python} (downloads the packages)"
"${python}" -m venv "${venv}"
"${venv}/bin/python" -m pip install --upgrade pip
"${venv}/bin/python" -m pip install -r "${target}/requirements.txt"
fi
say "building the C++ part"
make -C "${target}" -j"$(nproc)" all rph_radar_collector
echo "${archive##*/}" > "${target}/RELEASE"
trap - ERR
start_daemon
say "done ($("${venv}/bin/python" --version 2>&1)). Start it: cd '${target}' && ./start.sh"