Packaging: the wheel carries the PWA, so a release install serves the UI (v2.7.10)

- pyproject declared build-backend = "setuptools.backends._legacy:_Backend",
  which does not exist: `python -m build`, `pip install .` and even
  `pip install -e .` died with "Backend ... is not available".  It is
  setuptools.build_meta now.
- the wheel and sdist carried only the Python packages, and _default_web_dir()
  resolved <site-packages>/../frontend, so an installed server started fine but
  answered every static request with 404.  setup.py's build_py now copies the
  checkout's frontend/ into simtrace_analyser/web/ inside the build (the PWA's
  tests excluded), and _default_web_dir() prefers that packaged copy, falling
  back to <repo>/frontend for source and editable runs.  The source directory
  is resolved from setup.py (so an sdist build works) and a missing frontend/
  fails the build loudly instead of shipping a package without its UI.
- MANIFEST.in grafts frontend/ (with the READMEs, the udev rule and the four
  launch scripts) into the sdist, so a wheel built from the sdist has the PWA.
- README (EN/RU): an install-from-a-release section (pipx/pip with the wheel,
  the direct release URL for pip) and the --web-dir default documented as the
  bundled PWA, or <repo>/frontend from source.
- tests/test_packaging.py pins the packaged-first resolution (including the
  empty-directory fallback), the source checkout default, the real build
  backend, the manifest graft and setup.py's bundling.
- verified: a fresh venv install from the wheel and from the sdist serve
  GET / (200, byte-identical index.html, PWA v2.7.10) out of
  site-packages/simtrace_analyser/web; an editable install still resolves
  <repo>/frontend.  547 Python + 80 Node tests.
This commit is contained in:
catarrh committed 2026-10-20 23:06:33 +03:00
1 parent 4a03f83459
commit 69b200db11
11 files changed
+196 -16

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
graft frontend
include README.md README_RUS.md 70-simtrace2-pysniff.rules
include start.sh start.bat sniff-start.sh sniff-start.bat
+26 -3
View File
@@ -108,12 +108,34 @@ pip install -e .
# or: simtrace2-pysniff [opts]
```
### From a release (no git checkout)
The release artifacts bundle the PWA, so an installed server serves the same UI
as a clone. Download the wheel from the
[latest release](https://github.com/anttro/simtrace-analyser/releases/latest)
and install it:
```sh
pipx install ./simtrace_analyser-2.7.10-py3-none-any.whl
# or: pip install ./simtrace_analyser-2.7.10-py3-none-any.whl
# then run (the UI is served at http://127.0.0.1:8081/):
simtrace-analyser-server --capture gsmtap
```
`pip` can also fetch the wheel straight from the release page:
```sh
pip install https://github.com/anttro/simtrace-analyser/releases/download/v2.7.10/simtrace_analyser-2.7.10-py3-none-any.whl
```
## Running
### Analysis server + PWA (`simtrace-analyser-server`)
Stores captured APDU traffic in SQLite and serves it over HTTP, together with
the bundled **SIMtrace Analyser** PWA (`frontend/`) on the same origin. Three
the bundled **SIMtrace Analyser** PWA (`frontend/` in a source checkout,
bundled into the package on install) on the same origin. Three
capture modes:
```sh
@@ -283,13 +305,14 @@ Server options:
--db FILE SQLite database path (default: ~/.simtrace-analyser/sessions.db)
--capture MODE gsmtap | direct | disabled (default: gsmtap)
--gsmtap-port PORT UDP port for the GSMTAP listener (default: 4729)
--web-dir PATH PWA static files directory (default: <repo>/frontend)
--web-dir PATH PWA static files directory (default: the PWA bundled in
the installed package, or <repo>/frontend from source)
```
Then open **http://127.0.0.1:8081/** in a browser — the PWA is served by the
server, so the UI and the API share an origin and no CORS/Private-Network-Access
setup is involved. Use `--web-dir PATH` to serve a different PWA directory
(default: `<repo>/frontend`).
(default: the bundled PWA, or `<repo>/frontend` in a source checkout).
### Hosted PWA (landing page)
+27 -3
View File
@@ -113,12 +113,34 @@ pip install -e .
# или: simtrace2-pysniff [opts]
```
### Из релиза (без клонирования репозитория)
Релизные артефакты содержат PWA внутри пакета, поэтому установленный сервер
отдаёт тот же интерфейс, что и при запуске из клона. Скачайте wheel со
страницы [последнего релиза](https://github.com/anttro/simtrace-analyser/releases/latest)
и установите его:
```sh
pipx install ./simtrace_analyser-2.7.10-py3-none-any.whl
# или: pip install ./simtrace_analyser-2.7.10-py3-none-any.whl
# затем запуск (интерфейс отдаётся на http://127.0.0.1:8081/):
simtrace-analyser-server --capture gsmtap
```
`pip` также может скачать wheel прямо со страницы релиза:
```sh
pip install https://github.com/anttro/simtrace-analyser/releases/download/v2.7.10/simtrace_analyser-2.7.10-py3-none-any.whl
```
## Запуск
### Анализирующий сервер + PWA (`simtrace-analyser-server`)
Сохраняет захваченный APDU-трафик в SQLite и отдаёт его по HTTP вместе
со встроенной PWA **SIMtrace Analyser** (`frontend/`) на том же origin.
со встроенной PWA **SIMtrace Analyser** (`frontend/` в исходном клоне, при
установке — внутри пакета) на том же origin.
Три режима захвата:
```sh
@@ -289,13 +311,15 @@ TS 51.011), поэтому FID, повторно используемый в р
--db FILE путь к базе SQLite (по умолчанию: ~/.simtrace-analyser/sessions.db)
--capture MODE gsmtap | direct | disabled (по умолчанию: gsmtap)
--gsmtap-port PORT UDP-порт листенера GSMTAP (по умолчанию: 4729)
--web-dir PATH каталог статических файлов PWA (по умолчанию: <repo>/frontend)
--web-dir PATH каталог статических файлов PWA (по умолчанию: PWA внутри
установленного пакета или <repo>/frontend при запуске из исходников)
```
Затем откройте **http://127.0.0.1:8081/** в браузере — PWA отдаётся
сервером, поэтому UI и API используют один origin, и настройка
CORS/Private-Network-Access не требуется. Используйте `--web-dir PATH`,
чтобы отдавать другой каталог PWA (по умолчанию: `<repo>/frontend`).
чтобы отдавать другой каталог PWA (по умолчанию: PWA внутри пакета или
`<repo>/frontend` при запуске из исходников).
### Размещённая PWA (лендинг)
+1 -1
View File
@@ -3389,7 +3389,7 @@ function onSearchChange() {
}
// ──────────────────── Versioning ────────────────────
const PWA_VERSION = '2.7.9';
const PWA_VERSION = '2.7.10';
const MIN_SERVER_VERSION = '2.3.0';
function parseVersion(v) {
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simtrace-analyser-v91';
const CACHE = 'simtrace-analyser-v92';
const PRECACHE = [
'index.html',
+2 -2
View File
@@ -1,10 +1,10 @@
[build-system]
requires = ["setuptools>=64"]
build-backend = "setuptools.backends._legacy:_Backend"
build-backend = "setuptools.build_meta"
[project]
name = "simtrace-analyser"
version = "2.7.9"
version = "2.7.10"
description = "SIMtrace Analyser — APDU trace analyser for GSMTAP/PCAP captures (bundles the simtrace2-pysniff utility)"
readme = {file = "README.md", content-type = "text/markdown"}
license = {text = "GPL-2.0-or-later"}
+35
View File
@@ -0,0 +1,35 @@
"""Bundle the PWA into the installed package.
The metadata lives in pyproject.toml; this file only adds the build step that
copies ``frontend/`` into ``simtrace_analyser/web/`` inside the build tree, so
a wheel (or an install from it) serves the same UI as a source checkout.
``setup.py`` is part of the sdist, and the source directory is resolved
relative to this file, so a wheel built from the unpacked sdist gets the PWA
too.
"""
import os
import shutil
from setuptools import setup
from setuptools.command.build_py import build_py
HERE = os.path.dirname(os.path.abspath(__file__))
WEB_SOURCE = os.path.join(HERE, 'frontend')
WEB_TARGET = os.path.join('simtrace_analyser', 'web')
class BuildPy(build_py):
def run(self):
super().run()
if not os.path.isdir(WEB_SOURCE):
raise RuntimeError(
'the PWA source directory is missing: %s (a build must not '
'ship a package without its user interface)' % WEB_SOURCE)
target = os.path.join(self.build_lib, WEB_TARGET)
shutil.copytree(
WEB_SOURCE, target, dirs_exist_ok=True,
ignore=shutil.ignore_patterns('tests', '__pycache__', '*.pyc'))
setup(cmdclass={'build_py': BuildPy})
+16 -4
View File
@@ -16,10 +16,20 @@ from .capture import CaptureManager, GsmtapListener, DirectSniffer
from .version import __version__
def _pick_web_dir(package_dir, repo_root):
"""The installed package's bundled PWA first, then the source checkout."""
packaged = os.path.join(package_dir, 'web')
if os.path.isfile(os.path.join(packaged, 'index.html')):
return packaged
return os.path.join(repo_root, 'frontend')
def _default_web_dir():
# <repo>/frontend, whether run from source or an editable install.
# __file__ is <repo>/simtrace_analyser/__main__.py → up two levels.
return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'frontend')
# A wheel/sdist install carries simtrace_analyser/web (copied there by
# setup.py); an editable install and a plain source run fall through to
# <repo>/frontend. __file__ is <repo>/simtrace_analyser/__main__.py.
pkg_dir = os.path.dirname(os.path.abspath(__file__))
return _pick_web_dir(pkg_dir, os.path.dirname(pkg_dir))
def main():
@@ -42,7 +52,9 @@ def main():
p.add_argument('--log-requests', action='store_true', default=False,
help='Log every HTTP request (off by default)')
p.add_argument('--web-dir', default=_default_web_dir(), metavar='PATH',
help='Directory with the simtrace-analyser PWA static files to serve (default: <repo>/frontend)')
help='Directory with the simtrace-analyser PWA static files to serve '
'(default: the PWA bundled in the installed package, '
'or <repo>/frontend in a source checkout)')
args = p.parse_args()
db = Database(args.db)
+1 -1
View File
@@ -1,3 +1,3 @@
"""Single source of truth for the SIMtrace Analyser version."""
__version__ = '2.7.9'
__version__ = '2.7.10'
+83
View File
@@ -0,0 +1,83 @@
"""Tests for the distributable packaging (bundled PWA, backend, manifest)."""
import os
import tempfile
import unittest
from simtrace_analyser.__main__ import _default_web_dir, _pick_web_dir
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def _touch(path):
with open(path, 'w') as f:
f.write('')
class TestPickWebDir(unittest.TestCase):
def test_prefers_packaged_pwa(self):
with tempfile.TemporaryDirectory() as tmp:
pkg = os.path.join(tmp, 'simtrace_analyser')
repo = os.path.join(tmp, 'repo')
os.makedirs(os.path.join(pkg, 'web'))
os.makedirs(os.path.join(repo, 'frontend'))
_touch(os.path.join(pkg, 'web', 'index.html'))
_touch(os.path.join(repo, 'frontend', 'index.html'))
self.assertEqual(_pick_web_dir(pkg, repo),
os.path.join(pkg, 'web'))
def test_falls_back_when_packaged_dir_is_missing(self):
with tempfile.TemporaryDirectory() as tmp:
pkg = os.path.join(tmp, 'simtrace_analyser')
repo = os.path.join(tmp, 'repo')
os.makedirs(pkg)
os.makedirs(os.path.join(repo, 'frontend'))
_touch(os.path.join(repo, 'frontend', 'index.html'))
self.assertEqual(_pick_web_dir(pkg, repo),
os.path.join(repo, 'frontend'))
def test_falls_back_when_packaged_dir_has_no_index(self):
# A stale/empty simtrace_analyser/web directory must not shadow the
# source checkout (e.g. an editable install of a half-built tree).
with tempfile.TemporaryDirectory() as tmp:
pkg = os.path.join(tmp, 'simtrace_analyser')
repo = os.path.join(tmp, 'repo')
os.makedirs(os.path.join(pkg, 'web'))
os.makedirs(os.path.join(repo, 'frontend'))
_touch(os.path.join(repo, 'frontend', 'index.html'))
self.assertEqual(_pick_web_dir(pkg, repo),
os.path.join(repo, 'frontend'))
class TestDefaultWebDir(unittest.TestCase):
def test_source_checkout(self):
web = _default_web_dir()
self.assertTrue(os.path.isfile(os.path.join(web, 'index.html')),
'no index.html in %s' % web)
class TestDistributionMetadata(unittest.TestCase):
def _read(self, name):
with open(os.path.join(REPO, name)) as f:
return f.read()
def test_build_backend_exists(self):
pyproject = self._read('pyproject.toml')
self.assertIn('build-backend = "setuptools.build_meta"', pyproject)
self.assertNotIn('backends._legacy', pyproject)
def test_manifest_ships_the_pwa(self):
# The sdist carries frontend/, so a wheel built from it has the PWA.
self.assertIn('graft frontend', self._read('MANIFEST.in'))
def test_setup_py_bundles_the_pwa(self):
setup_py = self._read('setup.py')
self.assertIn('frontend', setup_py)
self.assertIn('copytree', setup_py)
self.assertIn("'web'", setup_py)
self.assertIn("'build_py'", setup_py)
self.assertIn('ignore_patterns', setup_py)
if __name__ == '__main__':
unittest.main()
+1 -1
View File
@@ -224,7 +224,7 @@ class TestImportEndpoint(unittest.TestCase):
class TestVersion(unittest.TestCase):
def test_package_version(self):
from simtrace2_pysniff import __version__
self.assertEqual(__version__, '2.7.9')
self.assertEqual(__version__, '2.7.10')
def test_status_includes_version(self):
import os