lift_indicator_suite/just/host.just

618 lines
24 KiB
Text
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# =============================================================================
# host.just — операции на хост-машине (вне devcontainer)
# =============================================================================
TOOLS_DIR := env('TOOLS_DIR', justfile_directory() / 'tools/host')
HIL_DIR := justfile_directory() / 'tools/hil'
CACHE_DIR := env('CACHE_DIR', justfile_directory() / '.cache')
BUILD_DIR := env('BUILD_DIR', justfile_directory() / 'build')
_uname := `uname -s`
_venv := if _uname =~ "MINGW|MSYS|CYGWIN" { ".venv-host-win" } else { ".venv-host" }
JUST_MIN := "1.36.0"
UV_MIN := "0.4.0"
DOCKER_MIN := "24.0.0"
PLATFORM := os()
set working-directory := '..'
# =============================================================================
# ГРУППА: setup
# =============================================================================
[doc('Проверить версии и наличие зависимостей хоста (just, uv, docker)')]
[group('setup')]
check-deps:
#!/usr/bin/env bash
set -euo pipefail
BOLD="\033[1m"; GREEN="\033[0;32m"; RED="\033[0;31m"; YELLOW="\033[1;33m"; RESET="\033[0m"
ERRORS=0; WARNINGS=0
ok() { echo -e " ${GREEN}✅ ${1}${RESET}"; }
fail() { echo -e " ${RED}❌ ${1}${RESET}"; ERRORS=$((ERRORS+1)); }
warn() { echo -e " ${YELLOW}⚠️ ${1}${RESET}"; WARNINGS=$((WARNINGS+1)); }
semver_ge() {
local a="$1" b="$2"
local a1 a2 a3 b1 b2 b3
IFS='.' read -r a1 a2 a3 <<< "${a}"
IFS='.' read -r b1 b2 b3 <<< "${b}"
a1="${a1//[^0-9]/}"; a2="${a2//[^0-9]/}"; a3="${a3//[^0-9]/}"
b1="${b1//[^0-9]/}"; b2="${b2//[^0-9]/}"; b3="${b3//[^0-9]/}"
[[ "${a1:-0}" -gt "${b1:-0}" ]] && return 0
[[ "${a1:-0}" -lt "${b1:-0}" ]] && return 1
[[ "${a2:-0}" -gt "${b2:-0}" ]] && return 0
[[ "${a2:-0}" -lt "${b2:-0}" ]] && return 1
[[ "${a3:-0}" -ge "${b3:-0}" ]] && return 0
return 1
}
install_hint() {
local tool=$1
case "{{ PLATFORM }}" in
linux)
case "$tool" in
just) echo "uv tool install 'rust-just=={{ JUST_MIN }}'" ;;
uv) echo "curl -LsSf https://astral.sh/uv/install.sh | sh" ;;
docker) echo "https://docs.docker.com/engine/install/ubuntu/" ;;
esac ;;
macos)
case "$tool" in
just) echo "uv tool install 'rust-just=={{ JUST_MIN }}'" ;;
uv) echo "brew install uv" ;;
docker) echo "brew install --cask docker" ;;
esac ;;
windows)
case "$tool" in
just) echo "uv tool install 'rust-just=={{ JUST_MIN }}'" ;;
uv) echo 'powershell -c "irm https://astral.sh/uv/install.ps1 | iex"' ;;
docker) echo "winget install --id Docker.DockerDesktop" ;;
esac ;;
esac
}
check_tool() {
local name=$1 cmd=$2 min_ver=$3
shift 3
local ver_args=("${@:---version}")
if ! command -v "$cmd" &>/dev/null; then
fail "$name not found -- $(install_hint "$name")"
return
fi
local actual
actual=$("$cmd" "${ver_args[@]}" 2>&1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ -z "$actual" ]]; then
warn "$name found but version could not be detected"
return
fi
if semver_ge "$actual" "$min_ver"; then
ok "$name $actual (>= $min_ver)"
else
fail "$name $actual -- need >= $min_ver -- $(install_hint "$name")"
fi
}
echo ""
echo -e "${BOLD} Platform: {{ PLATFORM }}${RESET}"
echo ""
echo -e "${BOLD} Checking host dependencies...${RESET}"
echo ""
check_tool "just" "just" "{{ JUST_MIN }}"
check_tool "uv" "uv" "{{ UV_MIN }}"
check_tool "docker" "docker" "{{ DOCKER_MIN }}"
if [[ "{{ PLATFORM }}" == "linux" ]]; then
echo ""
echo -e "${BOLD} Checking udev rules (Linux only)...${RESET}"
echo ""
rules_file="/etc/udev/rules.d/99-nxp-mimxrt.rules"
udev_ok=true
grep -q "1fc9" "$rules_file" 2>/dev/null || { warn "udev: BootROM rule missing"; udev_ok=false; }
grep -q "15a2" "$rules_file" 2>/dev/null || { warn "udev: Flashloader rule missing"; udev_ok=false; }
if ! groups 2>/dev/null | grep -q plugdev; then
warn "User '$(whoami)' not in 'plugdev' group"
udev_ok=false
fi
if [[ "$udev_ok" == "false" ]]; then
warn "Run: just host::setup-udev"
else
ok "udev rules (NXP USB) + plugdev group"
fi
fi
echo ""
if [[ $ERRORS -gt 0 ]]; then
echo -e " ${RED}${BOLD}$ERRORS error(s) found.${RESET}"
exit 1
elif [[ $WARNINGS -gt 0 ]]; then
echo -e " ${YELLOW}${BOLD}$WARNINGS warning(s).${RESET}"
else
echo -e " ${GREEN}${BOLD}All dependencies satisfied.${RESET}"
fi
echo ""
[doc('Установить udev rules для NXP USB (только Linux)')]
[group('setup')]
setup-udev:
#!/usr/bin/env bash
set -euo pipefail
case "$(uname -s)" in
Linux*) ;;
*) echo " udev rules are Linux-only, skipping."; exit 0 ;;
esac
printf '%s\n%s\n%s\n%s\n' \
'# NXP BootROM -- SDP mode' \
'SUBSYSTEM=="usb", ATTR{idVendor}=="1fc9", ATTR{idProduct}=="0130", MODE="0666", GROUP="plugdev"' \
'# NXP Flashloader' \
'SUBSYSTEM=="usb", ATTR{idVendor}=="15a2", ATTR{idProduct}=="0073", MODE="0666", GROUP="plugdev"' \
| sudo tee /etc/udev/rules.d/99-nxp-mimxrt.rules > /dev/null
sudo udevadm control --reload-rules && sudo udevadm trigger
sudo usermod -a -G plugdev "$USER"
echo " ✅ udev rules installed. Re-login required."
[doc('Установить/обновить Python-зависимости tools/host (spsdk и др.)')]
[group('setup')]
setup-tools:
#!/usr/bin/env bash
set -euo pipefail
LOCK="{{ TOOLS_DIR }}/uv.lock"
STAMP="{{ CACHE_DIR }}/uv_lock.sha256"
mkdir -p "{{ CACHE_DIR }}"
current=$(sha256sum "$LOCK" 2>/dev/null | cut -d' ' -f1 || echo "missing")
previous=$(cat "$STAMP" 2>/dev/null || echo "none")
if [[ "$current" == "$previous" ]]; then
echo " ✅ tools/host deps up to date"
else
echo " 📦 Syncing tools/host deps..."
cd "{{ TOOLS_DIR }}" && uv sync
echo "$current" > "$STAMP"
echo " ✅ tools/host deps installed"
fi
[doc('Полная первичная настройка хоста: check-deps + setup-udev + setup-tools')]
[group('setup')]
bootstrap:
#!/usr/bin/env bash
set -euo pipefail
echo ""
echo " === Bootstrap: step 1/3 -- check-deps ==="
just host::check-deps
if [[ "{{ PLATFORM }}" == "linux" ]]; then
echo " === Bootstrap: step 2/3 -- setup-udev ==="
just host::setup-udev
just host::setup-m5-udev
else
echo " step 2/3 -- setup-udev skipped ({{ PLATFORM }})"
fi
echo " === Bootstrap: step 3/3 -- setup-tools ==="
just host::setup-tools
echo ""
echo " ✅ Bootstrap complete."
echo " Next: open project in VSCode -> 'Reopen in Container'"
echo ""
[doc('Добавить пользователя в группу dialout (для /dev/ttyACM* — M5Stack, Linux)')]
[group('setup')]
setup-m5-udev:
#!/usr/bin/env bash
set -euo pipefail
case "$(uname -s)" in
Linux*) ;;
*) echo " dialout group — Linux only, skipping."; exit 0 ;;
esac
if groups "$USER" | grep -q dialout; then
echo " ✅ User '${USER}' already in dialout"
else
sudo usermod -a -G dialout "$USER"
echo " ✅ Added '${USER}' to dialout. Re-login required."
fi
# =============================================================================
# ГРУППА: flash-sdp — USB Serial Downloader (SDP через ROM-загрузчик)
# Требование: плата в режиме Serial Downloader (BOOT_MODE = 01)
# =============================================================================
[doc('Прошить образ через USB SDP: flash <firmware_test|bootloader|app> <debug|release>')]
[group('flash-sdp')]
flash project type="release":
#!/usr/bin/env bash
set -euo pipefail
case "{{ type }}" in
debug) BUILD_TYPE="Debug" ;;
release) BUILD_TYPE="Release" ;;
*) echo " ❌ Unknown type: {{ type }}"; exit 1 ;;
esac
cd "{{ TOOLS_DIR }}" && UV_PROJECT_ENVIRONMENT={{ _venv }} uv run python flash_usb.py \
--firmware "{{ project }}" --build-type "${BUILD_TYPE}"
[doc('Прошить образ в RAM через USB SDP: flash-ram <firmware_test|bootloader|app> <debug|release>')]
[group('flash-sdp')]
flash-ram project type="debug":
#!/usr/bin/env bash
set -euo pipefail
case "{{ type }}" in
debug) BUILD_TYPE="Debug" ;;
release) BUILD_TYPE="Release" ;;
*) echo " ❌ Unknown type: {{ type }}"; exit 1 ;;
esac
cd "{{ TOOLS_DIR }}" && UV_PROJECT_ENVIRONMENT={{ _venv }} uv run python flash_usb.py \
--firmware "{{ project }}" --build-type "${BUILD_TYPE}" --ram-only
[doc('Прошить firmware_test Debug через USB SDP')]
[group('flash-sdp')]
flash-test-debug: (flash "firmware_test" "debug")
[doc('Прошить firmware_test Release через USB SDP')]
[group('flash-sdp')]
flash-test-release: (flash "firmware_test" "release")
[confirm("Flash bootloader + app (Release)?")]
[doc('Прошить bootloader + tft_app Release через USB SDP (производство)')]
[group('flash-sdp')]
flash-production: (flash "bootloader" "release") (flash "app" "release")
[confirm("Стереть всю Flash (W25Q128) через USB SDP? Все прошивки будут удалены.")]
[doc('Стереть всю Flash (W25Q128) через USB SDP (chip erase, ~30 с)')]
[group('flash-sdp')]
flash-sdp-erase:
cd "{{ TOOLS_DIR }}" && UV_PROJECT_ENVIRONMENT={{ _venv }} uv run python flash_usb.py \
--erase-chip
# =============================================================================
# ГРУППА: flash-swd — прошивка через SWD (MCU-Link, без смены BOOT_MODE)
# =============================================================================
_flash_swd := TOOLS_DIR / "flash_swd.py"
[doc('Прошить firmware_test Debug через SWD. Нужен power cycle после')]
[group('flash-swd')]
flash-swd-test-debug:
uv run --directory {{ HIL_DIR }} python {{ _flash_swd }} \
--firmware firmware_test --build-type Debug
[doc('Прошить firmware_test Release через SWD. Нужен power cycle после')]
[group('flash-swd')]
flash-swd-test-release:
uv run --directory {{ HIL_DIR }} python {{ _flash_swd }} \
--firmware firmware_test --build-type Release
[doc('Прошить bootloader Debug через SWD. Нужен power cycle после')]
[group('flash-swd')]
flash-swd-bootloader-debug:
uv run --directory {{ HIL_DIR }} python {{ _flash_swd }} \
--firmware bootloader --build-type Debug
[doc('Прошить bootloader Release через SWD. Нужен power cycle после')]
[group('flash-swd')]
flash-swd-bootloader-release:
uv run --directory {{ HIL_DIR }} python {{ _flash_swd }} \
--firmware bootloader --build-type Release
[doc('Прошить tft_app Debug через SWD. Нужен power cycle после')]
[group('flash-swd')]
flash-swd-app-debug:
uv run --directory {{ HIL_DIR }} python {{ _flash_swd }} \
--firmware app --build-type Debug
[doc('Прошить tft_app Release через SWD. Нужен power cycle после')]
[group('flash-swd')]
flash-swd-app-release:
uv run --directory {{ HIL_DIR }} python {{ _flash_swd }} \
--firmware app --build-type Release
[confirm("Стереть всю Flash (W25Q128)? Все прошивки будут удалены.")]
[doc('Стереть всю Flash (W25Q128) через SWD (chip erase). Нужен power cycle после')]
[group('flash-swd')]
flash-swd-erase:
uv run --directory {{ HIL_DIR }} pyocd erase \
--target {{ env('PYOCD_TARGET', 'mimxrt1050_quadspi') }} \
--frequency {{ env('PYOCD_FREQUENCY', '4000000') }} \
--chip -O resume_on_disconnect=False
# =============================================================================
# ГРУППА: hil — запуск HIL-тестов на реальном железе
# Сборка target-прошивок — в devcontainer: just build::build-hil
# =============================================================================
_hil_build := BUILD_DIR / "target-debug"
[doc('Запустить все не интерактивные HIL-тесты')]
[group('hil')]
hil-run:
HIL_BUILD_DIR={{ _hil_build }} \
uv run --directory {{ HIL_DIR }} pytest -m "not interactive and not usb_vcom" -v
[doc('Запустить все интерактивные HIL-тесты: кнопки, дисплей')]
[group('hil')]
hil-run-interactive:
HIL_BUILD_DIR={{ _hil_build }} \
uv run --directory {{ HIL_DIR }} pytest -m interactive -s -v
[doc('Запустить HIL-тест UART')]
[group('hil')]
hil-uart:
HIL_BUILD_DIR={{ _hil_build }} \
uv run --directory {{ HIL_DIR }} pytest 01_test_uart.py -v
[doc('Запустить HIL-тест оптоизолированных входов')]
[group('hil')]
hil-opto:
HIL_BUILD_DIR={{ _hil_build }} \
uv run --directory {{ HIL_DIR }} pytest 02_test_opto.py -v
[doc('Запустить HIL-тест кнопок')]
[group('hil')]
hil-button:
HIL_BUILD_DIR={{ _hil_build }} \
uv run --directory {{ HIL_DIR }} pytest 04_test_button.py -s -v
[doc('Запустить HIL тест bsp_can')]
[group('hil')]
hil-can:
HIL_BUILD_DIR={{ _hil_build }} \
uv run --directory {{ HIL_DIR }} pytest 03_test_can.py -v
[doc('Запустить HIL-тест USB CDC')]
[group('hil')]
hil-usb-cdc:
HIL_BUILD_DIR={{ _hil_build }} \
uv run --directory {{ HIL_DIR }} pytest 05_test_usb_cdc.py -v
[doc('Запустить HIL-тест оптовходов через firmware_test CDC (протокол v2)')]
[group('hil')]
hil-firmware-opto:
uv run --directory {{ HIL_DIR }} pytest 06_test_firmware_opto.py -v
[doc('Запустить HIL-тест CAN через firmware_test CDC (протокол v2)')]
[group('hil')]
hil-firmware-can:
uv run --directory {{ HIL_DIR }} pytest 06_test_firmware_can.py -v
[doc('Запустить все firmware_test CDC HIL-тесты (opto + can)')]
[group('hil')]
hil-firmware:
uv run --directory {{ HIL_DIR }} pytest 06_test_firmware_opto.py 06_test_firmware_can.py -v
# =============================================================================
# ГРУППА: debug — GDB-сервер для отладки из VSCode (devcontainer)
#
# Рабочий цикл А — прошивка уже в Flash:
# just host::debug-server → VSCode: 🐛 Debug: <project> → F5
#
# Рабочий цикл Б — прошить и отладить без USB SDP:
# just host::flash-swd-test-debug
# just host::debug-server → VSCode: 🐛 Debug: firmware_test → F5
#
# Важно: debug-server и flash-swd используют MCU-Link монопольно.
# Остановите сервер (Ctrl+C) перед flash-swd и наоборот.
# =============================================================================
_hil_dir := "tools/hil"
[doc('Запустить GDB-сервер. Оставить запущенным, подключаться из VSCode')]
[group('debug')]
debug-server:
uv run --directory {{ _hil_dir }} pyocd gdbserver \
--config {{ justfile_directory() }}/pyocd_debug.yaml \
--port {{ env('GDB_PORT', '3333') }} \
--persist \
--semihosting
[doc('Показать доступные builtin-таргеты pyOCD для MIMXRT')]
[group('debug')]
debug-list-targets:
uv run --directory {{ _hil_dir }} pyocd list --targets | grep -i mimx
# =============================================================================
# ГРУППА: m5 — работа с M5Stack StamPLC
# =============================================================================
M5_PORT := env('HIL_M5_PORT', '/dev/ttyACM1')
_m5_agent := justfile_directory() / 'tools/hil/m5/agent.py'
[doc('Показать подключённые M5Stack устройства (USB CDC)')]
[group('m5')]
m5-scan:
#!/usr/bin/env bash
set -euo pipefail
echo ""
echo " Сканируем устройства через mpremote..."
echo ""
devices=$(uv run --directory "{{ HIL_DIR }}" mpremote devs 2>&1)
if [[ -z "$devices" ]]; then
echo " ⚠️ Устройства не найдены"
echo " Проверьте USB-кабель и права доступа к порту"
echo " Linux: sudo usermod -a -G dialout \$USER && newgrp dialout"
echo " macOS: никаких дополнительных прав не требуется"
exit 1
fi
echo "$devices"
echo ""
# Подсветить известный VID:PID для Espressif (M5StamPLC JTAG/serial)
if echo "$devices" | grep -qi "303a:1001"; then
echo " ✅ M5StamPLC (Espressif USB JTAG/serial) обнаружен"
elif echo "$devices" | grep -qi "303a:"; then
echo " ✅ Espressif устройство обнаружено"
else
echo " ⚠️ M5StamPLC не найден среди устройств"
fi
echo ""
[doc('Открыть MicroPython REPL на M5Stack (Ctrl+X для выхода)')]
[group('m5')]
m5-repl:
uv run --directory {{ HIL_DIR }} mpremote connect {{ M5_PORT }} repl
[doc('Проверить версию MicroPython на M5Stack')]
[group('m5')]
m5-version:
uv run --directory "{{ HIL_DIR }}" mpremote connect "{{ M5_PORT }}" \
exec 'import sys; print("MicroPython:", sys.version); import machine; print("Freq:", machine.freq(), "Hz")'
[doc('Развернуть агент + запустить CLI одной командой (быстрый старт)')]
[group('m5')]
m5-start:
#!/usr/bin/env bash
set -euo pipefail
just host::m5-deploy
uv run --directory "{{ HIL_DIR }}" mpremote connect "{{ M5_PORT }}" reset
sleep 2
just host::m5-cli
[doc('Управление питанием таргета через M5 RLY1: m5-power <on|off>')]
[group('m5')]
m5-power state:
uv run --directory {{ HIL_DIR }} python m5/power.py \
--port {{ M5_PORT }} --state {{ state }}
[doc('Скопировать агент на M5Stack (tools/hil/m5_agent.py → /main.py)')]
[group('m5')]
m5-deploy:
#!/usr/bin/env bash
set -euo pipefail
echo " 📤 Копируем агент на M5Stack..."
uv run --directory "{{ HIL_DIR }}" mpremote connect "{{ M5_PORT }}" \
cp "{{ _m5_agent }}" :main.py
uv run --directory "{{ HIL_DIR }}" mpremote connect "{{ M5_PORT }}" reset
echo " ✅ m5_agent.py → /main.py"
echo " ✅ M5Stack перезапущен"
[doc('Запустить интерактивный CLI для ручного тестирования агента')]
[group('m5')]
m5-cli:
uv run --directory {{ HIL_DIR }} python m5/cli.py --port {{ M5_PORT }}
# =============================================================================
# ГРУППА: util
# =============================================================================
[doc('Найти подключённые NXP устройства (BootROM, flashloader, обычный режим)')]
[group('util')]
scan:
cd "{{ TOOLS_DIR }}" && UV_PROJECT_ENVIRONMENT={{ _venv }} uv run nxpdevscan
[doc('Получить статус SDP (BootROM режим, VID:PID 1FC9:0130)')]
[group('util')]
[no-cd]
sdp-status:
cd "{{ TOOLS_DIR }}" && UV_PROJECT_ENVIRONMENT={{ _venv }} uv run sdphost -u 0x1FC9,0x0130 -- error-status
[doc('Получить статус flashloader (VID:PID 15A2:0073)')]
[group('util')]
flashloader-status:
cd "{{ TOOLS_DIR }}" && UV_PROJECT_ENVIRONMENT={{ _venv }} uv run blhost -u 0x15A2,0x0073 -- get-property 1 0
[doc('Открыть UART монитор на MCU-Link VCOM (кроссплатформенный)')]
[group('util')]
uart-monitor:
uv run --directory {{ HIL_DIR }} python3 -m serial.tools.miniterm \
{{ env('HIL_VCOM_PORT') }} {{ env('HIL_VCOM_BAUD', '115200') }} \
--raw
# =============================================================================
# ГРУППА: pipeline
# =============================================================================
[doc('Входной контроль: прошить firmware_test Release + запустить HIL-тесты')]
[group('pipeline')]
incoming: flash-test-release
#!/usr/bin/env bash
set -euo pipefail
echo " ▶ Running HIL tests..."
just host::hil-run
[doc('Производственная прошивка: bootloader + tft_app Release')]
[group('pipeline')]
production: flash-production
@echo " ✅ Production firmware flashed"
# =============================================================================
# ГРУППА: service — TUI сервисного инженера
# =============================================================================
PRODUCTION_DIR := justfile_directory() / 'tools/production'
[doc('Установить/обновить зависимости TUI сервисного инженера')]
[group('service')]
service-setup:
#!/usr/bin/env bash
set -euo pipefail
echo " 📦 Syncing tools/production deps..."
cd "{{ PRODUCTION_DIR }}" && uv sync
echo " ✅ tools/production deps installed"
[doc('Запустить TUI сервисного инженера')]
[group('service')]
service-tui:
uv run --directory {{ PRODUCTION_DIR }} python main.py
[doc('Собрать standalone-бинарь TUI (PyInstaller → dist/service_tui)')]
[group('service')]
service-build:
#!/usr/bin/env bash
set -euo pipefail
echo " 🔨 Building standalone service-tui..."
cd "{{ PRODUCTION_DIR }}" && uv run pyinstaller \
--onefile \
--name service_tui \
--add-data "../shared:shared" \
main.py
echo " ✅ dist/service_tui ready"
# =============================================================================
# ГРУППА: package — сборка service-tui в исполняемый бандл (PyInstaller)
# =============================================================================
_prod_dir := justfile_directory() / 'tools/production'
_prod_build_dir := env('BUILD_DIR', justfile_directory() / 'build')
[doc('Собрать service-tui в PyInstaller-бандл + скопировать firmware/*_hab.bin')]
[group('package')]
package-tui:
#!/usr/bin/env bash
set -euo pipefail
cd "{{ _prod_dir }}"
echo " 📦 Running PyInstaller..."
uv run pyinstaller service_tui.spec --noconfirm
DIST="dist/service_tui"
echo " 📦 Copying firmware HAB images (build/<Type>/*_hab.bin)..."
found=0
for type_dir in "{{ _prod_build_dir }}"/Debug "{{ _prod_build_dir }}"/Release; do
[[ -d "$type_dir" ]] || continue
type_name=$(basename "$type_dir")
for hab in "$type_dir"/*_hab.bin; do
[[ -f "$hab" ]] || continue
mkdir -p "$DIST/firmware/$type_name"
cp "$hab" "$DIST/firmware/$type_name/"
found=1
done
done
if [[ "$found" -eq 0 ]]; then
echo " ⚠️ Ни один *_hab.bin не найден в {{ _prod_build_dir }} — соберите"
echo " прошивки заранее (just build::hab-all-release / hab-all-debug)."
fi
VERSION=$(grep -m1 '^version' pyproject.toml | sed -E 's/.*"(.+)".*/\1/')
case "$(uname -s)" in
Linux*) OS_TAG="linux" ;;
Darwin*) OS_TAG="macos" ;;
MINGW*|MSYS*|CYGWIN*) OS_TAG="windows" ;;
*) OS_TAG="unknown" ;;
esac
RELEASE_NAME="service-tui-v${VERSION}-${OS_TAG}"
rm -rf "dist/${RELEASE_NAME}"
mkdir -p "dist/${RELEASE_NAME}/custom_binaries"
mv "$DIST" "dist/${RELEASE_NAME}"
echo " ✅ dist/${RELEASE_NAME}"