From 2ecf7bb45c524909e1d1367aeb6b77dcec4bffa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sun, 25 Oct 2026 00:42:36 +0300 Subject: [PATCH] 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. --- CHANGELOG.md | 7 +++++ README.md | 3 +- README_RUS.md | 3 +- setup.bat | 27 +++++++++++++++- setup.sh | 18 +++++++++-- tests/test_setup_scripts.py | 63 +++++++++++++++++++++++++++++++++++++ 6 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 tests/test_setup_scripts.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bb3589..0907d3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ` 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 diff --git a/README.md b/README.md index 088a601..83d4384 100644 --- a/README.md +++ b/README.md @@ -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`. | diff --git a/README_RUS.md b/README_RUS.md index 106ce35..daaf7c3 100644 --- a/README_RUS.md +++ b/README_RUS.md @@ -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`. | diff --git a/setup.bat b/setup.bat index 03de8bf..72e0b53 100644 --- a/setup.bat +++ b/setup.bat @@ -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" diff --git a/setup.sh b/setup.sh index d526be8..602a6d3 100755 --- a/setup.sh +++ b/setup.sh @@ -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" diff --git a/tests/test_setup_scripts.py b/tests/test_setup_scripts.py new file mode 100644 index 0000000..40cbc43 --- /dev/null +++ b/tests/test_setup_scripts.py @@ -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()