feat(licenses): add backend license collection and JSON file, update license handling in the collector, and update build scripts to include backend licenses

This commit is contained in:
Ivan 2026-07-04 18:26:10 -05:00
parent 5bc10bfb89
commit db806684bc
No known key found for this signature in database
8 changed files with 4205 additions and 3949 deletions

View file

@ -30,6 +30,12 @@ frontend_licenses_path = (
if frontend_licenses_path.exists():
include_files.append((str(frontend_licenses_path), "licenses_frontend.json"))
backend_licenses_path = (
ROOT / "meshchatx" / "src" / "backend" / "data" / "licenses_backend.json"
)
if backend_licenses_path.exists():
include_files.append((str(backend_licenses_path), "licenses_backend.json"))
third_party_notices_path = (
ROOT / "meshchatx" / "src" / "backend" / "data" / "THIRD_PARTY_NOTICES.txt"
)

View file

@ -1,5 +1,5 @@
Reticulum MeshChatX - Third-party notices
Generated at: 2026-07-04T22:04:30.591013Z
Generated at: 2026-07-04T22:50:15.826526Z
Frontend source: pnpm
Python dependencies
@ -19,6 +19,9 @@ aiosignal 1.4.0
attrs 26.1.0
License: MIT
Author: Hynek Schlawack <hs@ox.cx>
audioop-lts 0.2.2
License: PSF-2.0
Author: —
bcrypt 5.0.0
License: Apache-2.0
Author: The Python Cryptographic Authority developers <cryptography-dev@python.org>
@ -79,9 +82,6 @@ reticulum-meshchatx 4.7.2
rns 1.3.7
License: Reticulum License
Author: Mark Qvist
typing_extensions 4.15.0
License: PSF-2.0
Author: "Guido van Rossum, Jukka Lehtosalo, Łukasz Langa, Michael Lee" <levkivskyi@gmail.com>
websockets 16.0
License: BSD-3-Clause
Author: Aymeric Augustin <aymeric.augustin@m4x.org>

View file

@ -0,0 +1,170 @@
[
{
"name": "aiohappyeyeballs",
"version": "2.6.1",
"author": "J. Nick Koston",
"license": "PSF-2.0"
},
{
"name": "aiohttp",
"version": "3.14.1",
"author": "—",
"license": "Apache-2.0 AND MIT"
},
{
"name": "aiohttp-session",
"version": "2.12.1",
"author": "Andrew Svetlov",
"license": "Apache 2"
},
{
"name": "aiosignal",
"version": "1.4.0",
"author": "aiohttp team <team@aiohttp.org>",
"license": "Apache 2.0"
},
{
"name": "attrs",
"version": "26.1.0",
"author": "Hynek Schlawack <hs@ox.cx>",
"license": "MIT"
},
{
"name": "audioop-lts",
"version": "0.2.2",
"author": "—",
"license": "PSF-2.0"
},
{
"name": "bcrypt",
"version": "5.0.0",
"author": "The Python Cryptographic Authority developers <cryptography-dev@python.org>",
"license": "Apache-2.0"
},
{
"name": "cbor2",
"version": "6.1.1",
"author": "Alex Grönholm <alex.gronholm@nextday.fi>",
"license": "MIT"
},
{
"name": "cffi",
"version": "2.0.0",
"author": "Armin Rigo, Maciej Fijalkowski",
"license": "MIT"
},
{
"name": "cryptography",
"version": "49.0.0",
"author": "The Python Cryptographic Authority and individual contributors <cryptography-dev@python.org>",
"license": "Apache-2.0 OR BSD-3-Clause"
},
{
"name": "frozenlist",
"version": "1.8.0",
"author": "aiohttp team <team@aiohttp.org>",
"license": "Apache-2.0"
},
{
"name": "idna",
"version": "3.18",
"author": "Kim Davies <kim+pypi@gumleaf.org>",
"license": "BSD-3-Clause"
},
{
"name": "lxmf",
"version": "1.0.1",
"author": "Mark Qvist",
"license": "Reticulum License"
},
{
"name": "lxmfy",
"version": "1.6.2",
"author": "Quad4 <team@quad4.io>",
"license": "BSD-0-Clause"
},
{
"name": "lxst",
"version": "0.4.8",
"author": "Mark Qvist",
"license": "Other/Proprietary License"
},
{
"name": "miniaudio",
"version": "1.71",
"author": "Irmen de Jong <irmen@razorvine.net>",
"license": "MIT"
},
{
"name": "multidict",
"version": "6.7.1",
"author": "Andrew Svetlov",
"license": "Apache License 2.0"
},
{
"name": "numpy",
"version": "2.4.6",
"author": "Travis E. Oliphant et al.",
"license": "BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0"
},
{
"name": "ply",
"version": "3.11",
"author": "David Beazley",
"license": "BSD"
},
{
"name": "propcache",
"version": "0.4.1",
"author": "Andrew Svetlov",
"license": "Apache-2.0"
},
{
"name": "psutil",
"version": "7.2.2",
"author": "Giampaolo Rodola",
"license": "BSD-3-Clause"
},
{
"name": "pycodec2",
"version": "4.1.1",
"author": "Grzegorz Milka",
"license": "OSI Approved :: BSD License"
},
{
"name": "pycparser",
"version": "3.0",
"author": "Eli Bendersky <eliben@gmail.com>",
"license": "BSD-3-Clause"
},
{
"name": "pyserial",
"version": "3.5",
"author": "Chris Liechti",
"license": "BSD"
},
{
"name": "reticulum-meshchatx",
"version": "4.7.2",
"author": "Quad4",
"license": "0BSD AND MIT"
},
{
"name": "rns",
"version": "1.3.7",
"author": "Mark Qvist",
"license": "Reticulum License"
},
{
"name": "websockets",
"version": "16.0",
"author": "Aymeric Augustin <aymeric.augustin@m4x.org>",
"license": "BSD-3-Clause"
},
{
"name": "yarl",
"version": "1.23.0",
"author": "Andrew Svetlov",
"license": "Apache-2.0"
}
]

File diff suppressed because it is too large Load diff

View file

@ -21,6 +21,7 @@ from packaging.utils import canonicalize_name
_ROOT_DIST_CANDIDATES = ("reticulum-meshchatx", "reticulum_meshchatx")
_DATA_SUBPATH = Path("meshchatx") / "src" / "backend" / "data"
_FRONTEND_LICENSES_FILENAME = "licenses_frontend.json"
_BACKEND_LICENSES_FILENAME = "licenses_backend.json"
_THIRD_PARTY_NOTICES_FILENAME = "THIRD_PARTY_NOTICES.txt"
@ -186,7 +187,7 @@ def _merge_bundled_lxmfy(
return merged
def collect_backend_licenses() -> list[dict[str, Any]]:
def _collect_backend_licenses_live() -> list[dict[str, Any]]:
repo = _repo_root()
for root in _ROOT_DIST_CANDIDATES:
if _dist_for_requirement_name(root) is not None:
@ -195,6 +196,30 @@ def collect_backend_licenses() -> list[dict[str, Any]]:
return _merge_bundled_lxmfy(repo, _collect_python_transitive(roots))
def _load_embedded_backend_licenses() -> list[dict[str, Any]] | None:
for path in _embedded_data_paths(_BACKEND_LICENSES_FILENAME):
if not path.is_file():
continue
try:
raw = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
continue
if not isinstance(raw, list):
continue
return [x for x in raw if isinstance(x, dict)]
return None
def collect_backend_licenses() -> list[dict[str, Any]]:
embedded = _load_embedded_backend_licenses()
if getattr(sys, "frozen", False) and embedded:
return embedded
live = _collect_backend_licenses_live()
if live:
return live
return embedded or []
def _license_from_package_json(data: dict[str, Any]) -> str:
lic = data.get("license")
if isinstance(lic, str) and lic.strip():
@ -497,11 +522,14 @@ def write_embedded_license_artifacts(repo_root: Path | None = None) -> dict[str,
data_dir.mkdir(parents=True, exist_ok=True)
payload = build_licenses_payload()
frontend = payload.get("frontend", [])
backend = payload.get("backend", [])
meta = payload.get("meta", {}) if isinstance(payload, dict) else {}
frontend_source = str(meta.get("frontend_source", "unknown"))
frontend_path = data_dir / _FRONTEND_LICENSES_FILENAME
backend_path = data_dir / _BACKEND_LICENSES_FILENAME
notices_path = data_dir / _THIRD_PARTY_NOTICES_FILENAME
frontend_rows = frontend if isinstance(frontend, list) else []
backend_rows = backend if isinstance(backend, list) else []
should_write_frontend = (
bool(frontend_rows)
or not frontend_path.exists()
@ -516,11 +544,17 @@ def write_embedded_license_artifacts(repo_root: Path | None = None) -> dict[str,
json.dumps(frontend_rows, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
backend_path.write_text(
json.dumps(backend_rows, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
notices_path.write_text(render_third_party_notices(payload), encoding="utf-8")
return {
"frontend_path": str(frontend_path),
"frontend_count": len(frontend_rows),
"frontend_written": should_write_frontend,
"backend_path": str(backend_path),
"backend_count": len(backend_rows),
"notices_path": str(notices_path),
}

View file

@ -53,7 +53,7 @@ exclude = ["tests*"]
namespaces = false
[tool.setuptools.package-data]
meshchatx = ["public/**/*", "public/*", "src/backend/data/community_interfaces.json", "src/backend/data/licenses_frontend.json", "src/backend/data/THIRD_PARTY_NOTICES.txt", "src/frontend/public/repository-server-index.html"]
meshchatx = ["public/**/*", "public/*", "src/backend/data/community_interfaces.json", "src/backend/data/licenses_frontend.json", "src/backend/data/licenses_backend.json", "src/backend/data/THIRD_PARTY_NOTICES.txt", "src/frontend/public/repository-server-index.html"]
[tool.setuptools.exclude-package-data]
meshchatx = ["public/repository-server-bundled/**"]

View file

@ -173,8 +173,13 @@ function shouldRefreshLicenseArtifacts(repoRoot) {
const dataDir = path.join(repoRoot, "meshchatx", "src", "backend", "data");
const noticesPath = path.join(dataDir, "THIRD_PARTY_NOTICES.txt");
const frontendLicensesPath = path.join(dataDir, "licenses_frontend.json");
const backendLicensesPath = path.join(dataDir, "licenses_backend.json");
const outputTimes = [fileMtimeMs(noticesPath), fileMtimeMs(frontendLicensesPath)];
const outputTimes = [
fileMtimeMs(noticesPath),
fileMtimeMs(frontendLicensesPath),
fileMtimeMs(backendLicensesPath),
];
if (outputTimes.some((t) => t == null)) {
return true;
}

View file

@ -7,6 +7,7 @@ from meshchatx.src.backend.licenses_collector import (
_filter_out_workspace_root_package,
_flatten_pnpm_licenses_json,
build_licenses_payload,
collect_backend_licenses,
collect_frontend_from_node_modules,
render_third_party_notices,
write_embedded_license_artifacts,
@ -131,10 +132,13 @@ def test_write_embedded_license_artifacts_writes_files(tmp_path):
result = write_embedded_license_artifacts(repo_root=tmp_path)
frontend_path = Path(result["frontend_path"])
backend_path = Path(result["backend_path"])
notices_path = Path(result["notices_path"])
assert frontend_path.exists()
assert backend_path.exists()
assert notices_path.exists()
assert '"name": "vue"' in frontend_path.read_text(encoding="utf-8")
assert '"name": "rns"' in backend_path.read_text(encoding="utf-8")
assert "Reticulum MeshChatX - Third-party notices" in notices_path.read_text(
encoding="utf-8"
)
@ -161,3 +165,40 @@ def test_write_embedded_license_artifacts_preserves_existing_frontend_when_empty
assert result["frontend_written"] is False
assert frontend_path.read_text(encoding="utf-8") == '[{"name":"kept"}]\n'
def test_collect_backend_licenses_uses_embedded_when_frozen():
embedded = [{"name": "rns", "version": "1.0", "author": "a", "license": "MIT"}]
with (
patch("meshchatx.src.backend.licenses_collector._load_embedded_backend_licenses", return_value=embedded),
patch("meshchatx.src.backend.licenses_collector._collect_backend_licenses_live", return_value=[]),
patch("meshchatx.src.backend.licenses_collector.sys") as mock_sys,
):
mock_sys.frozen = True
rows = collect_backend_licenses()
assert rows == embedded
def test_collect_backend_licenses_prefers_live_over_embedded_when_not_frozen():
embedded = [{"name": "stale", "version": "0", "author": "a", "license": "MIT"}]
live = [{"name": "rns", "version": "2.0", "author": "b", "license": "MIT"}]
with (
patch("meshchatx.src.backend.licenses_collector._load_embedded_backend_licenses", return_value=embedded),
patch("meshchatx.src.backend.licenses_collector._collect_backend_licenses_live", return_value=live),
patch("meshchatx.src.backend.licenses_collector.sys") as mock_sys,
):
mock_sys.frozen = False
rows = collect_backend_licenses()
assert rows == live
def test_collect_backend_licenses_falls_back_to_embedded_when_live_empty():
embedded = [{"name": "rns", "version": "1.0", "author": "a", "license": "MIT"}]
with (
patch("meshchatx.src.backend.licenses_collector._load_embedded_backend_licenses", return_value=embedded),
patch("meshchatx.src.backend.licenses_collector._collect_backend_licenses_live", return_value=[]),
patch("meshchatx.src.backend.licenses_collector.sys") as mock_sys,
):
mock_sys.frozen = False
rows = collect_backend_licenses()
assert rows == embedded