setup: recreate an incomplete .venv instead of activating it

A failed venv creation (e.g. the Debian/Ubuntu python3-venv package
missing) left the .venv directory behind; the next run saw it, skipped
creation and failed to source/activate a venv without activate/pip.
setup.sh now validates bin/activate, bin/python and bin/pip, removes an
incomplete directory and creates it again; a creation failure removes the
partial directory and names the venv package.  setup.bat does the same
with Scripts\activate.bat/python.exe/pip.exe and !errorlevel!.

READMEs (prerequisites + scripts table), CHANGELOG (folded into the
unreleased 3.38.1) and AGENTS updated.

Tests: tests/test_setup_scripts.py (5: the shell/bat source pins and
bash -n).  1088 Python + 1075 frontend.
This commit is contained in:
2026-10-25 00:42:36 +03:00
parent a269f8bd2e
commit 2ecf7bb45c
6 changed files with 116 additions and 5 deletions
+7
View File
@@ -37,6 +37,13 @@ every change carries a descriptive commit message.
exits 2; `start.sh` runs the same access pre-flight before launching. An
explicit `-d <device>` keeps the serial path, and a reader present but no card
is still a normal cardless start.
- **`setup.sh`/`setup.bat` recover from an incomplete `.venv`.** A failed venv
creation (e.g. the Debian/Ubuntu `python3-venv` package missing) left the
directory behind; the next run saw `.venv` and skipped creation, then tried to
activate a venv without `activate`/`pip`. The scripts now validate
`bin/activate`/`bin/python`/`bin/pip` (`Scripts\…` on Windows), remove an
incomplete directory and create it again; a creation failure removes the
partial directory and points at the venv package. The READMEs note it.
## [3.38.0] — 2026-10-24
+2 -1
View File
@@ -748,6 +748,7 @@ The bundled Python server wraps [pySim](https://osmocom.org/projects/pysim/wiki)
### Prerequisites
- **Python 3.9+** with `pip`, and **Git** — the app runs on 3.9+, but the **SCP81 TLS-PSK listener needs Python 3.13+** (`ssl`'s PSK callbacks).
- **Python venv support** — `setup.sh` creates `.venv`; if that fails with `ensurepip is not available` (Debian/Ubuntu ship the venv module separately), install it (`sudo apt install python3-venv`) and run `./setup.sh` again. An incomplete `.venv` left by the failed run is removed automatically.
- **Smart card reader** (PC/SC or serial/FTDI) — PC/SC is preferable (`pcsc-lite` + `ccid` on Linux). `pyscard` has no Linux wheels, so it builds from source: install the PC/SC header package too (`libpcsclite-dev` on Debian/Ubuntu, `pcsc-lite-devel` on Fedora; on Arch `pcsclite` includes them).
- **Windows** — use **Python 3.13 (strongly recommended)**: `pyscard` has precompiled wheels up to 3.13 and the SCP81 TLS-PSK listener needs 3.13+. On 3.14 `pyscard` and some pysim dependency wheels are not available and build from source (needs MSVC C++ Build Tools). 3.9–3.12 run the app but not the SCP81 PSK listener. The SMPP bridge (`smpp.twisted3`) is intentionally skipped on Windows.
@@ -755,7 +756,7 @@ The bundled Python server wraps [pySim](https://osmocom.org/projects/pysim/wiki)
| Script | What it does |
|--------|-------------|
| `setup.sh` / `setup.bat` | Creates `.venv/`, installs pysim and the server. Run once after cloning or after unzipping the release package. |
| `setup.sh` / `setup.bat` | Creates `.venv/` (or repairs an incomplete one), installs pysim and the server. Run once after cloning or after unzipping the release package. |
| `start.sh` / `start.bat` | Starts the server from the venv (serves the PWA + API on `:8080`). |
| `update.sh` / `update.bat` | Updates a packaged install in place: fetches the latest release package, replaces the app files and re-installs the app into the existing `.venv` (pysim is kept). `--check`, `--version X.Y.Z`, `--zip FILE`, `--no-pip`, `--force`. |
+2 -1
View File
@@ -732,6 +732,7 @@ PWA проверяет версию сервера при подключении
### Требования
- **Python 3.9+** с `pip`, и **Git** — приложение работает на 3.9+, но **SCP81 TLS-PSK-слушателю нужен Python 3.13+** (PSK-callback'и `ssl`).
- **Поддержка venv в Python** — `setup.sh` создаёт `.venv`; если создание завершается с `ensurepip is not available` (в Debian/Ubuntu модуль venv поставляется отдельно), установите его (`sudo apt install python3-venv`) и запустите `./setup.sh` снова. Неполный `.venv`, оставшийся от неудачного запуска, удаляется автоматически.
- **Смарт-картридер** (PC/SC или serial/FTDI) — предпочтителен PC/SC (`pcsc-lite` + `ccid` на Linux). У `pyscard` нет Linux-wheel, поэтому он собирается из исходников: поставьте и пакет с заголовками PC/SC (`libpcsclite-dev` в Debian/Ubuntu, `pcsc-lite-devel` в Fedora; в Arch они входят в `pcsclite`).
- **Windows** — используйте **Python 3.13 (настоятельно рекомендуется)**: у `pyscard` есть готовые wheel до 3.13, и SCP81 TLS-PSK-слушателю нужен 3.13+. На 3.14 wheel для `pyscard` и части зависимостей pysim отсутствуют, они собираются из исходников (нужны MSVC C++ Build Tools). 3.9–3.12 запускают приложение, но не SCP81 PSK-слушатель. SMPP-мост (`smpp.twisted3`) в Windows сознательно не ставится.
@@ -739,7 +740,7 @@ PWA проверяет версию сервера при подключении
| Скрипт | Назначение |
|--------|-------------|
| `setup.sh` / `setup.bat` | Создаёт `.venv/`, ставит pysim и сервер. Запускать один раз после клонирования или распаковки релизного пакета. |
| `setup.sh` / `setup.bat` | Создаёт `.venv/` (или исправляет неполный), ставит pysim и сервер. Запускать один раз после клонирования или распаковки релизного пакета. |
| `start.sh` / `start.bat` | Запускает сервер из venv (раздаёт PWA + API на `:8080`). |
| `update.sh` / `update.bat` | Обновляет пакетную установку на месте: скачивает последний релизный пакет, заменяет файлы приложения и переустанавливает приложение в существующий `.venv` (pysim сохраняется). `--check`, `--version X.Y.Z`, `--zip FILE`, `--no-pip`, `--force`. |
+26 -1
View File
@@ -34,10 +34,35 @@ echo Found Git
git --version
echo.
REM Create venv
REM Create the venv. A failed creation leaves a partial .venv behind: remove
REM it so the next run starts over (v3.38.1).
set "VENV_OK="
if exist "%VENV_DIR%" (
set "VENV_OK=1"
if not exist "%VENV_DIR%\Scripts\activate.bat" set "VENV_OK="
if not exist "%VENV_DIR%\Scripts\python.exe" set "VENV_OK="
if not exist "%VENV_DIR%\Scripts\pip.exe" set "VENV_OK="
if not defined VENV_OK (
echo Removing an incomplete virtual environment (a previous setup did not finish)...
rmdir /s /q "%VENV_DIR%"
)
)
if not exist "%VENV_DIR%" (
echo Creating virtual environment...
python -m venv "%VENV_DIR%"
if !errorlevel! neq 0 (
if exist "%VENV_DIR%" rmdir /s /q "%VENV_DIR%"
echo Error: could not create the virtual environment.
echo Make sure the Python installation includes venv and pip.
echo Run the Python installer, choose Modify, enable pip/venv, then run setup.bat again.
pause
exit /b 1
)
)
if not exist "%VENV_DIR%\Scripts\activate.bat" (
echo Error: the virtual environment has no Scripts\activate.bat.
pause
exit /b 1
)
call "%VENV_DIR%\Scripts\activate.bat"
+16 -2
View File
@@ -14,10 +14,24 @@ command -v git >/dev/null 2>&1 || { echo "Error: Git is required but not found.
PY_VER=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
echo "Found Python $PY_VER"
# Create venv
# Create the venv. A failed creation (missing venv/ensurepip) leaves a partial
# .venv behind: validate it and remove it so the next run starts over (v3.38.1).
if [ -d "$VENV_DIR" ] && { [ ! -f "$VENV_DIR/bin/activate" ] || [ ! -x "$VENV_DIR/bin/python" ] || [ ! -x "$VENV_DIR/bin/pip" ]; }; then
echo "Removing an incomplete virtual environment (a previous setup did not finish)..."
rm -rf "$VENV_DIR"
fi
if [ ! -d "$VENV_DIR" ]; then
echo "Creating virtual environment..."
python3 -m venv "$VENV_DIR"
if ! python3 -m venv "$VENV_DIR"; then
rm -rf "$VENV_DIR"
echo "Error: could not create the virtual environment."
echo " The Python venv/ensurepip component is missing or broken."
echo " Debian/Ubuntu: sudo apt install python3-venv"
echo " Fedora: sudo dnf install python3-libs"
echo " Arch: venv and ensurepip are part of python"
echo " Install it and run ./setup.sh again."
exit 1
fi
fi
source "$VENV_DIR/bin/activate"
PIP="$VENV_DIR/bin/pip"
+63
View File
@@ -0,0 +1,63 @@
"""The setup scripts recover from an incomplete .venv (v3.38.1).
A failed venv creation (the Debian/Ubuntu python3-venv package missing) used to
leave the .venv directory behind; the next run saw it, skipped creation and
failed to activate a venv without activate/pip.
"""
import shutil
import subprocess
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
class SetupShTests(unittest.TestCase):
def setUp(self):
self.src = (ROOT / 'setup.sh').read_text()
def test_an_incomplete_venv_is_removed_before_creation(self):
self.assertIn('Removing an incomplete virtual environment', self.src)
self.assertIn('rm -rf "$VENV_DIR"', self.src)
self.assertIn('bin/activate', self.src)
self.assertIn('bin/python', self.src)
self.assertIn('bin/pip', self.src)
def test_a_failed_creation_removes_the_partial_dir_and_guides(self):
self.assertIn('if ! python3 -m venv "$VENV_DIR"; then', self.src)
self.assertIn('python3-venv', self.src)
fails = self.src[self.src.index('if ! python3 -m venv'):]
self.assertLess(fails.index('rm -rf "$VENV_DIR"'),
fails.index('could not create the virtual environment'))
def test_the_script_is_valid_bash(self):
bash = shutil.which('bash')
if not bash:
self.skipTest('bash not available')
res = subprocess.run([bash, '-n', str(ROOT / 'setup.sh')],
capture_output=True, text=True)
self.assertEqual(res.returncode, 0, res.stderr)
class SetupBatTests(unittest.TestCase):
def setUp(self):
self.src = (ROOT / 'setup.bat').read_text()
def test_an_incomplete_venv_is_removed_before_creation(self):
self.assertIn('Removing an incomplete virtual environment', self.src)
self.assertIn('rmdir /s /q "%VENV_DIR%"', self.src)
for marker in ('Scripts\\activate.bat', 'Scripts\\python.exe',
'Scripts\\pip.exe'):
self.assertIn(marker, self.src)
def test_a_failed_creation_removes_the_partial_dir_and_guides(self):
self.assertIn('if !errorlevel! neq 0', self.src)
self.assertIn('python -m venv "%VENV_DIR%"', self.src)
fails = self.src[self.src.index('if !errorlevel! neq 0'):]
self.assertLess(fails.index('rmdir /s /q "%VENV_DIR%"'),
fails.index('could not create the virtual environment'))
if __name__ == '__main__':
unittest.main()