Release a Windows-friendly runtime zip (v2.7.12)
The releases carried the wheel and the sdist; the only zip was GitHub's source archive of the whole repository (tests, CI, tools). A Windows user just wants to extract a folder and double-click start.bat. - scripts/build_release_zip.py builds simtrace-analyser-<version>.zip with a single top-level directory: the two Python packages, the PWA runtime files (frontend/tests and package.json stay out), the four launchers, the udev rule, the READMEs and the screenshot. The .bat entries are written with CRLF line endings (cmd.exe mishandles bare-LF labels/goto), the .sh files keep their executable bit; LICENSE is included automatically once the repo has one. - tests/test_release_zip.py pins the archive shape, the exclusions, the sw.js PRECACHE drift (every precached asset must travel), the CRLF/LF line endings and start.sh's mode. - the release workflow builds the zip, checks it carries start.bat and the PWA, uploads it with the run and attaches it to the release. - READMEs (EN/RU) and the release-notes template offer the zip as the Windows-friendly option; version 2.7.12, PWA cache v94. Verified: the built zip (34 entries, 658 KB compressed) extracted and started on Linux serves the same index.html; 553 Python + 80 Node tests.
This commit is contained in:
1 parent
b19abbd514
commit
84e3cf9787
11 files changed
+280
-10
No files matched your search
@@ -12,3 +12,9 @@ virtual environment first (`python3 -m venv .venv && . .venv/bin/activate`).
|
|||||||
The source archive is the same tree a clone would have: unzip it and run
|
The source archive is the same tree a clone would have: unzip it and run
|
||||||
`./start.sh` (Linux/macOS) or `start.bat` (Windows). Direct SIMtrace2 capture
|
`./start.sh` (Linux/macOS) or `start.bat` (Windows). Direct SIMtrace2 capture
|
||||||
on Linux additionally needs the attached `70-simtrace2-pysniff.rules` udev rule.
|
on Linux additionally needs the attached `70-simtrace2-pysniff.rules` udev rule.
|
||||||
|
|
||||||
|
**Runtime zip (Windows-friendly, no installer)** — `simtrace-analyser-*.zip`
|
||||||
|
is the runtime tree only (packages, PWA, launchers, docs — no tests, no git):
|
||||||
|
extract it anywhere and run `start.bat` (Windows) or `./start.sh`
|
||||||
|
(Linux/macOS). Python 3 has to be on `PATH`; `pip install pyusb` is only
|
||||||
|
needed for direct SIMtrace2 capture.
|
||||||
@@ -49,6 +49,22 @@ jobs:
|
|||||||
- name: Build the sdist and the wheel
|
- name: Build the sdist and the wheel
|
||||||
run: python -m build --sdist --wheel
|
run: python -m build --sdist --wheel
|
||||||
|
|
||||||
|
- name: Build the runtime zip
|
||||||
|
run: python scripts/build_release_zip.py --output dist
|
||||||
|
|
||||||
|
- name: The runtime zip is complete
|
||||||
|
run: |
|
||||||
|
python - <<'PY'
|
||||||
|
import glob
|
||||||
|
import zipfile
|
||||||
|
archive = glob.glob('dist/*.zip')[0]
|
||||||
|
names = zipfile.ZipFile(archive).namelist()
|
||||||
|
for required in ('start.bat', 'frontend/index.html'):
|
||||||
|
assert any(n.endswith('/' + required) for n in names), \
|
||||||
|
'%s is missing from %s' % (required, archive)
|
||||||
|
print('%s: %d entries, launchers and PWA bundled' % (archive, len(names)))
|
||||||
|
PY
|
||||||
|
|
||||||
- name: The wheel bundles the PWA
|
- name: The wheel bundles the PWA
|
||||||
run: |
|
run: |
|
||||||
python - <<'PY'
|
python - <<'PY'
|
||||||
@@ -96,4 +112,4 @@ jobs:
|
|||||||
--title "$GITHUB_REF_NAME" \
|
--title "$GITHUB_REF_NAME" \
|
||||||
--generate-notes \
|
--generate-notes \
|
||||||
--notes-file .github/release-notes.md \
|
--notes-file .github/release-notes.md \
|
||||||
dist/*.whl dist/*.tar.gz 70-simtrace2-pysniff.rules
|
dist/*.whl dist/*.tar.gz dist/*.zip 70-simtrace2-pysniff.rules
|
||||||
@@ -105,7 +105,7 @@ Everything is on the
|
|||||||
**Wheel** (the PWA is inside the package):
|
**Wheel** (the PWA is inside the package):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pip install ./simtrace_analyser-2.7.11-py3-none-any.whl
|
pip install ./simtrace_analyser-2.7.12-py3-none-any.whl
|
||||||
|
|
||||||
# then run (the UI is served at http://127.0.0.1:8081/):
|
# then run (the UI is served at http://127.0.0.1:8081/):
|
||||||
simtrace-analyser-server --capture gsmtap
|
simtrace-analyser-server --capture gsmtap
|
||||||
@@ -119,7 +119,7 @@ Fedora), install into a virtual environment first:
|
|||||||
shown there):
|
shown there):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pip install https://github.com/anttro/simtrace-analyser/releases/download/v2.7.11/simtrace_analyser-2.7.11-py3-none-any.whl
|
pip install https://github.com/anttro/simtrace-analyser/releases/download/v2.7.12/simtrace_analyser-2.7.12-py3-none-any.whl
|
||||||
```
|
```
|
||||||
|
|
||||||
With the GitHub CLI, download the latest wheel without knowing the version:
|
With the GitHub CLI, download the latest wheel without knowing the version:
|
||||||
@@ -133,6 +133,12 @@ pip install ./simtrace_analyser-*.whl
|
|||||||
without the git history: unzip, `cd` into it and use the launchers below
|
without the git history: unzip, `cd` into it and use the launchers below
|
||||||
(`./start.sh` on Linux/macOS, `start.bat` on Windows).
|
(`./start.sh` on Linux/macOS, `start.bat` on Windows).
|
||||||
|
|
||||||
|
**Zip (Windows-friendly, no installer)** — `simtrace-analyser-2.7.12.zip` is
|
||||||
|
the runtime tree only (packages, PWA, launchers, docs; no tests, no git):
|
||||||
|
download it, extract it anywhere and run `start.bat` (Windows) or
|
||||||
|
`./start.sh` (Linux/macOS). Python 3 has to be on `PATH`; `pip install pyusb`
|
||||||
|
is only needed for direct SIMtrace2 capture.
|
||||||
|
|
||||||
### From a git clone (development)
|
### From a git clone (development)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
+8
-2
@@ -111,7 +111,7 @@ git) или склонировать репозиторий для разраб
|
|||||||
**Wheel** (PWA внутри пакета):
|
**Wheel** (PWA внутри пакета):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pip install ./simtrace_analyser-2.7.11-py3-none-any.whl
|
pip install ./simtrace_analyser-2.7.12-py3-none-any.whl
|
||||||
|
|
||||||
# затем запуск (интерфейс отдаётся на http://127.0.0.1:8081/):
|
# затем запуск (интерфейс отдаётся на http://127.0.0.1:8081/):
|
||||||
simtrace-analyser-server --capture gsmtap
|
simtrace-analyser-server --capture gsmtap
|
||||||
@@ -125,7 +125,7 @@ Fedora), сначала создайте виртуальное окружени
|
|||||||
указанную там):
|
указанную там):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pip install https://github.com/anttro/simtrace-analyser/releases/download/v2.7.11/simtrace_analyser-2.7.11-py3-none-any.whl
|
pip install https://github.com/anttro/simtrace-analyser/releases/download/v2.7.12/simtrace_analyser-2.7.12-py3-none-any.whl
|
||||||
```
|
```
|
||||||
|
|
||||||
С GitHub CLI можно скачать wheel последнего релиза, не зная версии:
|
С GitHub CLI можно скачать wheel последнего релиза, не зная версии:
|
||||||
@@ -139,6 +139,12 @@ pip install ./simtrace_analyser-*.whl
|
|||||||
дерево без истории git: распакуйте, перейдите в каталог и используйте скрипты
|
дерево без истории git: распакуйте, перейдите в каталог и используйте скрипты
|
||||||
запуска ниже (`./start.sh` в Linux/macOS, `start.bat` в Windows).
|
запуска ниже (`./start.sh` в Linux/macOS, `start.bat` в Windows).
|
||||||
|
|
||||||
|
**Zip (для Windows, без установки)** — `simtrace-analyser-2.7.12.zip`
|
||||||
|
содержит только рабочее дерево (пакеты, PWA, скрипты запуска, документацию;
|
||||||
|
без тестов и git): скачайте, распакуйте в любое место и запустите `start.bat`
|
||||||
|
(Windows) или `./start.sh` (Linux/macOS). Python 3 должен быть в `PATH`;
|
||||||
|
`pip install pyusb` нужен только для прямого захвата с SIMtrace2.
|
||||||
|
|
||||||
### Из клона репозитория (для разработки)
|
### Из клона репозитория (для разработки)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
+1
-1
@@ -3389,7 +3389,7 @@ function onSearchChange() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ──────────────────── Versioning ────────────────────
|
// ──────────────────── Versioning ────────────────────
|
||||||
const PWA_VERSION = '2.7.11';
|
const PWA_VERSION = '2.7.12';
|
||||||
const MIN_SERVER_VERSION = '2.3.0';
|
const MIN_SERVER_VERSION = '2.3.0';
|
||||||
|
|
||||||
function parseVersion(v) {
|
function parseVersion(v) {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simtrace-analyser-v93';
|
const CACHE = 'simtrace-analyser-v94';
|
||||||
|
|
||||||
const PRECACHE = [
|
const PRECACHE = [
|
||||||
'index.html',
|
'index.html',
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "simtrace-analyser"
|
name = "simtrace-analyser"
|
||||||
version = "2.7.11"
|
version = "2.7.12"
|
||||||
description = "SIMtrace Analyser — APDU trace analyser for GSMTAP/PCAP captures (bundles the simtrace2-pysniff utility)"
|
description = "SIMtrace Analyser — APDU trace analyser for GSMTAP/PCAP captures (bundles the simtrace2-pysniff utility)"
|
||||||
readme = {file = "README.md", content-type = "text/markdown"}
|
readme = {file = "README.md", content-type = "text/markdown"}
|
||||||
license = {text = "GPL-2.0-or-later"}
|
license = {text = "GPL-2.0-or-later"}
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
"""Build the ready-to-run runtime zip for a GitHub release.
|
||||||
|
|
||||||
|
The wheel needs ``pip`` and the GitHub source archive carries the whole
|
||||||
|
repository (tests, CI, the gitignored ``tools/``). This zip is the runtime
|
||||||
|
tree only: extract it anywhere, run ``start.bat`` on Windows or
|
||||||
|
``./start.sh`` elsewhere, and the server serves the bundled PWA. Python 3
|
||||||
|
must be on ``PATH``; PyUSB is only needed for direct hardware capture.
|
||||||
|
|
||||||
|
The build is deliberately simple - the explicit include lists below are the
|
||||||
|
whole manifest, so nothing from ``tests/``, ``.github/``, ``pyproject.toml``
|
||||||
|
or a build directory can leak into the archive by accident.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parents[1]
|
||||||
|
# The script lives outside the packages; import the version from the checkout.
|
||||||
|
sys.path.insert(0, str(REPO))
|
||||||
|
|
||||||
|
from simtrace_analyser.version import __version__ # noqa: E402
|
||||||
|
|
||||||
|
# The Python packages (all modules, no caches).
|
||||||
|
PACKAGES = ('simtrace_analyser', 'simtrace2_pysniff')
|
||||||
|
|
||||||
|
# The PWA runtime files; ``tests/`` and ``package.json`` are dev-only.
|
||||||
|
FRONTEND = 'frontend'
|
||||||
|
FRONTEND_EXCLUDE = ('tests', 'package.json')
|
||||||
|
|
||||||
|
# Repo-root files that make the extracted tree usable. LICENSE is included
|
||||||
|
# once the repository has one.
|
||||||
|
ROOT_FILES = (
|
||||||
|
'start.sh',
|
||||||
|
'start.bat',
|
||||||
|
'sniff-start.sh',
|
||||||
|
'sniff-start.bat',
|
||||||
|
'70-simtrace2-pysniff.rules',
|
||||||
|
'README.md',
|
||||||
|
'README_RUS.md',
|
||||||
|
'frontend.png',
|
||||||
|
'LICENSE',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Skip editor/byte-code droppings wherever they appear.
|
||||||
|
SKIP_DIRS = ('__pycache__',)
|
||||||
|
SKIP_NAMES = ('.DS_Store',)
|
||||||
|
SKIP_SUFFIXES = ('.pyc',)
|
||||||
|
|
||||||
|
|
||||||
|
def _files(root):
|
||||||
|
"""Yield the regular files under *root*, sorted and cache-free."""
|
||||||
|
for path in sorted(root.rglob('*')):
|
||||||
|
if not path.is_file():
|
||||||
|
continue
|
||||||
|
if any(part in SKIP_DIRS for part in path.parts):
|
||||||
|
continue
|
||||||
|
if path.name in SKIP_NAMES or path.suffix in SKIP_SUFFIXES:
|
||||||
|
continue
|
||||||
|
yield path
|
||||||
|
|
||||||
|
|
||||||
|
def _write_bat(zf, path, arcname):
|
||||||
|
"""Write a .bat entry with CRLF line endings.
|
||||||
|
|
||||||
|
The checkout stores the batch files with LF, and cmd.exe mishandles a
|
||||||
|
batch file whose lines end in a bare LF (labels and ``goto`` in
|
||||||
|
particular), so the archive gets the Windows form. The source files
|
||||||
|
stay untouched. ``writestr`` does not copy the file mode, so set it.
|
||||||
|
"""
|
||||||
|
data = path.read_bytes().replace(b'\r\n', b'\n').replace(b'\n', b'\r\n')
|
||||||
|
info = zipfile.ZipInfo.from_file(path, arcname)
|
||||||
|
info.external_attr = 0o644 << 16
|
||||||
|
info.compress_type = zipfile.ZIP_DEFLATED
|
||||||
|
zf.writestr(info, data)
|
||||||
|
|
||||||
|
|
||||||
|
def build_zip(output_dir, version=None):
|
||||||
|
"""Write ``simtrace-analyser-<version>.zip`` into *output_dir*.
|
||||||
|
|
||||||
|
Returns the archive path. The zip has a single top-level directory
|
||||||
|
``simtrace-analyser-<version>/`` holding the runtime tree.
|
||||||
|
"""
|
||||||
|
version = version or __version__
|
||||||
|
output_dir = Path(output_dir)
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
archive = output_dir / ('simtrace-analyser-%s.zip' % version)
|
||||||
|
top = 'simtrace-analyser-%s' % version
|
||||||
|
|
||||||
|
with zipfile.ZipFile(archive, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
for package in PACKAGES:
|
||||||
|
for path in _files(REPO / package):
|
||||||
|
zf.write(path, '%s/%s' % (top, path.relative_to(REPO).as_posix()))
|
||||||
|
|
||||||
|
for path in _files(REPO / FRONTEND):
|
||||||
|
rel = path.relative_to(REPO / FRONTEND)
|
||||||
|
if rel.parts[0] in FRONTEND_EXCLUDE:
|
||||||
|
continue
|
||||||
|
zf.write(path, '%s/%s' % (top, path.relative_to(REPO).as_posix()))
|
||||||
|
|
||||||
|
for name in ROOT_FILES:
|
||||||
|
path = REPO / name
|
||||||
|
if not path.is_file():
|
||||||
|
# LICENSE is not in the repository yet; include it once it is.
|
||||||
|
continue
|
||||||
|
arcname = '%s/%s' % (top, name)
|
||||||
|
if path.suffix == '.bat':
|
||||||
|
_write_bat(zf, path, arcname)
|
||||||
|
else:
|
||||||
|
zf.write(path, arcname)
|
||||||
|
|
||||||
|
return archive
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Build the runtime zip attached to a release')
|
||||||
|
parser.add_argument('--output', default='dist', metavar='DIR',
|
||||||
|
help='directory for the archive (default: dist)')
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
archive = build_zip(args.output)
|
||||||
|
with zipfile.ZipFile(archive) as zf:
|
||||||
|
entries = len(zf.namelist())
|
||||||
|
print(archive)
|
||||||
|
print('%d entries' % entries)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
"""Single source of truth for the SIMtrace Analyser version."""
|
"""Single source of truth for the SIMtrace Analyser version."""
|
||||||
|
|
||||||
__version__ = '2.7.11'
|
__version__ = '2.7.12'
|
||||||
@@ -224,7 +224,7 @@ class TestImportEndpoint(unittest.TestCase):
|
|||||||
class TestVersion(unittest.TestCase):
|
class TestVersion(unittest.TestCase):
|
||||||
def test_package_version(self):
|
def test_package_version(self):
|
||||||
from simtrace2_pysniff import __version__
|
from simtrace2_pysniff import __version__
|
||||||
self.assertEqual(__version__, '2.7.11')
|
self.assertEqual(__version__, '2.7.12')
|
||||||
|
|
||||||
def test_status_includes_version(self):
|
def test_status_includes_version(self):
|
||||||
import os
|
import os
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
"""Tests for the ready-to-run release zip (scripts/build_release_zip.py)."""
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import re
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from simtrace_analyser.version import __version__
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
# scripts/ is not a package, so load the builder from its path.
|
||||||
|
_spec = importlib.util.spec_from_file_location(
|
||||||
|
'build_release_zip', REPO / 'scripts' / 'build_release_zip.py')
|
||||||
|
build_release_zip = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(build_release_zip)
|
||||||
|
|
||||||
|
|
||||||
|
class TestReleaseZip(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls._tmp = tempfile.TemporaryDirectory()
|
||||||
|
cls.archive = build_release_zip.build_zip(cls._tmp.name)
|
||||||
|
with zipfile.ZipFile(cls.archive) as zf:
|
||||||
|
cls.names = zf.namelist()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
cls._tmp.cleanup()
|
||||||
|
|
||||||
|
def test_name_and_single_top_level_directory(self):
|
||||||
|
top = 'simtrace-analyser-%s' % __version__
|
||||||
|
self.assertEqual(self.archive.name, top + '.zip')
|
||||||
|
for name in self.names:
|
||||||
|
self.assertTrue(name.startswith(top + '/'),
|
||||||
|
'%s is outside %s/' % (name, top))
|
||||||
|
|
||||||
|
def test_required_entries(self):
|
||||||
|
top = 'simtrace-analyser-%s' % __version__
|
||||||
|
for rel in (
|
||||||
|
'simtrace_analyser/__main__.py',
|
||||||
|
'simtrace_analyser/version.py',
|
||||||
|
'simtrace2_pysniff/__main__.py',
|
||||||
|
'frontend/index.html',
|
||||||
|
'frontend/sw.js',
|
||||||
|
'frontend/manifest.json',
|
||||||
|
'start.sh',
|
||||||
|
'start.bat',
|
||||||
|
'sniff-start.sh',
|
||||||
|
'sniff-start.bat',
|
||||||
|
'70-simtrace2-pysniff.rules',
|
||||||
|
'README.md'):
|
||||||
|
self.assertIn('%s/%s' % (top, rel), self.names)
|
||||||
|
|
||||||
|
def test_dev_only_entries_are_excluded(self):
|
||||||
|
for name in self.names:
|
||||||
|
self.assertNotIn('/tests/', name)
|
||||||
|
self.assertNotIn('/__pycache__/', name)
|
||||||
|
self.assertNotIn('.pyc', name)
|
||||||
|
self.assertNotIn('/.git', name)
|
||||||
|
self.assertNotIn('/.github/', name)
|
||||||
|
self.assertNotIn(
|
||||||
|
'simtrace-analyser-%s/frontend/package.json' % __version__,
|
||||||
|
self.names)
|
||||||
|
self.assertNotIn(
|
||||||
|
'simtrace-analyser-%s/pyproject.toml' % __version__, self.names)
|
||||||
|
|
||||||
|
def test_precache_files_are_in_the_zip(self):
|
||||||
|
# Drift guard: every asset the service worker precaches must travel
|
||||||
|
# with the PWA, otherwise an offline install fails on a fresh extract.
|
||||||
|
text = (REPO / 'frontend' / 'sw.js').read_text()
|
||||||
|
match = re.search(r'PRECACHE\s*=\s*\[(.*?)\]', text, re.S)
|
||||||
|
self.assertIsNotNone(match, 'no PRECACHE array in frontend/sw.js')
|
||||||
|
assets = re.findall(r"'([^']+)'", match.group(1))
|
||||||
|
self.assertTrue(assets)
|
||||||
|
top = 'simtrace-analyser-%s' % __version__
|
||||||
|
for asset in assets:
|
||||||
|
self.assertIn('%s/frontend/%s' % (top, asset), self.names,
|
||||||
|
'precached %s is missing from the zip' % asset)
|
||||||
|
|
||||||
|
def test_batch_files_use_crlf(self):
|
||||||
|
with zipfile.ZipFile(self.archive) as zf:
|
||||||
|
bats = [n for n in self.names if n.endswith('.bat')]
|
||||||
|
self.assertTrue(bats)
|
||||||
|
for name in bats:
|
||||||
|
data = zf.read(name)
|
||||||
|
self.assertIn(b'\r\n', data, name)
|
||||||
|
self.assertNotIn(b'\n', data.replace(b'\r\n', b''),
|
||||||
|
'%s has a bare LF' % name)
|
||||||
|
|
||||||
|
def test_shell_script_is_lf_and_executable(self):
|
||||||
|
name = 'simtrace-analyser-%s/start.sh' % __version__
|
||||||
|
with zipfile.ZipFile(self.archive) as zf:
|
||||||
|
info = zf.getinfo(name)
|
||||||
|
data = zf.read(name)
|
||||||
|
self.assertNotIn(b'\r\n', data)
|
||||||
|
self.assertTrue(info.external_attr >> 16 & 0o100,
|
||||||
|
'start.sh lost its executable bit')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in new issue
Block a user