#!/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 (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 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 dir in "${backup}"/*/; do name="$(basename -- "${dir}")" case "${name}" in build|graphify-out|device_firmware|docs) 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" cp -al -- "${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 echo "${archive##*/}" > "${target}/RELEASE" trap - ERR start_daemon say "done ($("${venv}/bin/python" --version 2>&1)). Start it: cd '${target}' && ./start.sh"