From 25aa87ba1e9b869b0d195a28d3aa9d0c47a14abc 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: Tue, 20 Oct 2026 23:06:33 +0300 Subject: [PATCH] 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 /../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 /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 /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 /frontend. 547 Python + 80 Node tests. --- MANIFEST.in | 3 ++ README.md | 29 ++++++++++-- README_RUS.md | 30 +++++++++++-- frontend/index.html | 2 +- frontend/sw.js | 2 +- pyproject.toml | 4 +- setup.py | 35 +++++++++++++++ simtrace_analyser/__main__.py | 20 +++++++-- simtrace_analyser/version.py | 2 +- tests/test_packaging.py | 83 +++++++++++++++++++++++++++++++++++ tests/test_pcap_import.py | 2 +- 11 files changed, 196 insertions(+), 16 deletions(-) create mode 100644 MANIFEST.in create mode 100644 setup.py create mode 100644 tests/test_packaging.py diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..03102c2 --- /dev/null +++ b/MANIFEST.in @@ -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 diff --git a/README.md b/README.md index ccb0b2e..0b41fc9 100644 --- a/README.md +++ b/README.md @@ -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: /frontend) +--web-dir PATH PWA static files directory (default: the PWA bundled in + the installed package, or /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: `/frontend`). +(default: the bundled PWA, or `/frontend` in a source checkout). ### Hosted PWA (landing page) diff --git a/README_RUS.md b/README_RUS.md index 79f46a6..6fab958 100644 --- a/README_RUS.md +++ b/README_RUS.md @@ -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 (по умолчанию: /frontend) +--web-dir PATH каталог статических файлов PWA (по умолчанию: PWA внутри + установленного пакета или /frontend при запуске из исходников) ``` Затем откройте **http://127.0.0.1:8081/** в браузере — PWA отдаётся сервером, поэтому UI и API используют один origin, и настройка CORS/Private-Network-Access не требуется. Используйте `--web-dir PATH`, -чтобы отдавать другой каталог PWA (по умолчанию: `/frontend`). +чтобы отдавать другой каталог PWA (по умолчанию: PWA внутри пакета или +`/frontend` при запуске из исходников). ### Размещённая PWA (лендинг) diff --git a/frontend/index.html b/frontend/index.html index af1792d..c1b4220 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -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) { diff --git a/frontend/sw.js b/frontend/sw.js index 3c1c6db..01032d4 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simtrace-analyser-v91'; +const CACHE = 'simtrace-analyser-v92'; const PRECACHE = [ 'index.html', diff --git a/pyproject.toml b/pyproject.toml index a2d31a6..72553ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"} diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..24db703 --- /dev/null +++ b/setup.py @@ -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}) diff --git a/simtrace_analyser/__main__.py b/simtrace_analyser/__main__.py index 506940e..b0eeee6 100644 --- a/simtrace_analyser/__main__.py +++ b/simtrace_analyser/__main__.py @@ -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(): - # /frontend, whether run from source or an editable install. - # __file__ is /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 + # /frontend. __file__ is /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: /frontend)') + help='Directory with the simtrace-analyser PWA static files to serve ' + '(default: the PWA bundled in the installed package, ' + 'or /frontend in a source checkout)') args = p.parse_args() db = Database(args.db) diff --git a/simtrace_analyser/version.py b/simtrace_analyser/version.py index cd3138d..cf327b7 100644 --- a/simtrace_analyser/version.py +++ b/simtrace_analyser/version.py @@ -1,3 +1,3 @@ """Single source of truth for the SIMtrace Analyser version.""" -__version__ = '2.7.9' +__version__ = '2.7.10' diff --git a/tests/test_packaging.py b/tests/test_packaging.py new file mode 100644 index 0000000..6aaca17 --- /dev/null +++ b/tests/test_packaging.py @@ -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() diff --git a/tests/test_pcap_import.py b/tests/test_pcap_import.py index bddf362..537fe0d 100644 --- a/tests/test_pcap_import.py +++ b/tests/test_pcap_import.py @@ -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