feat(licenses): add functionality to collect and write frontend licenses and third-party notices; implement embedded data paths and update build process to include license artifacts

This commit is contained in:
Ivan committed 2026-04-15 03:11:13 -05:00
1 parent 7fd826abb0
commit c71ae17535
4 files changed
+238 -13

No files matched your search

+12
View File
@@ -18,6 +18,18 @@ changelog_path = ROOT / "CHANGELOG.md"
if changelog_path.exists():
include_files.append((str(changelog_path), "CHANGELOG.md"))
frontend_licenses_path = (
ROOT / "meshchatx" / "src" / "backend" / "data" / "licenses_frontend.json"
)
if frontend_licenses_path.exists():
include_files.append((str(frontend_licenses_path), "licenses_frontend.json"))
third_party_notices_path = (
ROOT / "meshchatx" / "src" / "backend" / "data" / "THIRD_PARTY_NOTICES.txt"
)
if third_party_notices_path.exists():
include_files.append((str(third_party_notices_path), "THIRD_PARTY_NOTICES.txt"))
if PUBLIC_DIR.exists() and PUBLIC_DIR.is_dir():
include_files.append((str(PUBLIC_DIR), "public"))
@@ -0,0 +1,91 @@
Reticulum MeshChatX - Third-party notices
Generated at: 2026-04-15T07:29:13.270941Z
Frontend source: embedded
Python dependencies
-------------------
aiohappyeyeballs 2.6.1
License: PSF-2.0
Author: J. Nick Koston
aiohttp 3.13.5
License: Apache-2.0 AND MIT
Author: —
aiohttp-session 2.12.1
License: Apache 2
Author: Andrew Svetlov
aiosignal 1.4.0
License: Apache 2.0
Author: aiohttp team <team@aiohttp.org>
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>
cffi 2.0.0
License: MIT
Author: Armin Rigo, Maciej Fijalkowski
cryptography 46.0.7
License: Apache-2.0 OR BSD-3-Clause
Author: The Python Cryptographic Authority and individual contributors <cryptography-dev@python.org>
frozenlist 1.8.0
License: Apache-2.0
Author: aiohttp team <team@aiohttp.org>
idna 3.11
License: BSD-3-Clause
Author: Kim Davies <kim+pypi@gumleaf.org>
jaraco.context 6.1.2
License: MIT
Author: "Jason R. Coombs" <jaraco@jaraco.com>
lxmf 0.9.4
License: Reticulum License
Author: Mark Qvist
lxmfy 1.6.1
License: BSD-0-Clause
Author: Quad4
lxst 0.4.6
License: Other/Proprietary License
Author: Mark Qvist
multidict 6.7.1
License: Apache License 2.0
Author: Andrew Svetlov
numpy 2.4.4
License: BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0
Author: Travis E. Oliphant et al.
ply 3.11
License: BSD
Author: David Beazley
propcache 0.4.1
License: Apache-2.0
Author: Andrew Svetlov
psutil 7.2.2
License: BSD-3-Clause
Author: Giampaolo Rodola
pycodec2 4.1.1
License: OSI Approved :: BSD License
Author: Grzegorz Milka
pycparser 3.0
License: BSD-3-Clause
Author: Eli Bendersky <eliben@gmail.com>
pyserial 3.5
License: BSD
Author: Chris Liechti
reticulum-meshchatx 4.4.0
License: MIT
Author: Sudo-Ivan
rns 1.1.5
License: Reticulum License
Author: Mark Qvist
websockets 16.0
License: BSD-3-Clause
Author: Aymeric Augustin <aymeric.augustin@m4x.org>
yarl 1.23.0
License: Apache-2.0
Author: Andrew Svetlov
Node dependencies
-----------------
No entries.
+111 -11
View File
@@ -6,6 +6,7 @@ import importlib.metadata
import json
import shutil
import subprocess
import sys
import tomllib
from datetime import UTC, datetime
from pathlib import Path
@@ -15,6 +16,9 @@ from packaging.requirements import Requirement
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"
_THIRD_PARTY_NOTICES_FILENAME = "THIRD_PARTY_NOTICES.txt"
def _repo_root() -> Path:
@@ -162,18 +166,35 @@ def _flatten_pnpm_licenses_json(data: dict[str, Any]) -> list[dict[str, Any]]:
return out
def _embedded_data_paths(filename: str) -> list[Path]:
paths = [Path(__file__).resolve().parent / "data" / filename]
exe_parent = Path(sys.executable).resolve().parent
paths.append(exe_parent / filename)
paths.append(exe_parent / "data" / filename)
paths.append(exe_parent / _DATA_SUBPATH / filename)
seen: set[str] = set()
unique_paths: list[Path] = []
for path in paths:
key = str(path)
if key in seen:
continue
seen.add(key)
unique_paths.append(path)
return unique_paths
def _load_embedded_frontend_licenses() -> list[dict[str, Any]] | None:
data_dir = Path(__file__).resolve().parent / "data"
path = data_dir / "licenses_frontend.json"
if not path.is_file():
return None
try:
raw = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return None
if not isinstance(raw, list):
return None
return [x for x in raw if isinstance(x, dict)]
for path in _embedded_data_paths(_FRONTEND_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_frontend_licenses() -> tuple[list[dict[str, Any]], str]:
@@ -221,3 +242,82 @@ def build_licenses_payload() -> dict[str, Any]:
"frontend_source": fe_source,
},
}
def render_third_party_notices(payload: dict[str, Any]) -> str:
"""Render third-party dependency notices as plain text."""
meta = payload.get("meta", {}) if isinstance(payload, dict) else {}
generated_at = str(meta.get("generated_at", "unknown"))
frontend_source = str(meta.get("frontend_source", "unknown"))
lines = [
"Reticulum MeshChatX - Third-party notices",
f"Generated at: {generated_at}",
f"Frontend source: {frontend_source}",
"",
]
sections: list[tuple[str, list[dict[str, Any]]]] = [
("Python dependencies", payload.get("backend", [])),
("Node dependencies", payload.get("frontend", [])),
]
for title, rows in sections:
lines.append(title)
lines.append("-" * len(title))
if not isinstance(rows, list) or not rows:
lines.append("No entries.")
lines.append("")
continue
for row in rows:
if not isinstance(row, dict):
continue
name = str(row.get("name", "?"))
version = str(row.get("version", "?"))
author = str(row.get("author", "—"))
license_name = str(row.get("license", "—"))
lines.append(f"{name} {version}")
lines.append(f" License: {license_name}")
lines.append(f" Author: {author}")
lines.append("")
return "\n".join(lines).rstrip() + "\n"
def write_embedded_license_artifacts(repo_root: Path | None = None) -> dict[str, Any]:
"""Generate and write embedded license metadata and notices artifacts."""
if repo_root is None:
repo_root = _repo_root()
data_dir = repo_root / _DATA_SUBPATH
data_dir.mkdir(parents=True, exist_ok=True)
payload = build_licenses_payload()
frontend = payload.get("frontend", [])
meta = payload.get("meta", {}) if isinstance(payload, dict) else {}
frontend_source = str(meta.get("frontend_source", "unknown"))
frontend_path = data_dir / _FRONTEND_LICENSES_FILENAME
notices_path = data_dir / _THIRD_PARTY_NOTICES_FILENAME
frontend_rows = frontend if isinstance(frontend, list) else []
should_write_frontend = (
bool(frontend_rows) or not frontend_path.exists() or frontend_source == "pnpm"
)
if should_write_frontend:
frontend_path.write_text(
json.dumps(frontend_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,
"notices_path": str(notices_path),
}
def main() -> int:
if len(sys.argv) > 1 and sys.argv[1] == "--write-artifacts":
result = write_embedded_license_artifacts()
print(json.dumps(result, indent=2))
return 0
print(json.dumps(build_licenses_payload(), indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+24 -2
View File
@@ -91,11 +91,33 @@ try {
const cmdParts = pythonCmd.trim().split(/\s+/).filter(Boolean);
const cmd = cmdParts[0];
const args = [...cmdParts.slice(1), "cx_setup.py", "build"];
const baseArgs = cmdParts.slice(1);
const licensesArgs = [...baseArgs, "-m", "meshchatx.src.backend.licenses_collector", "--write-artifacts"];
const args = [...baseArgs, "cx_setup.py", "build"];
let spawnCmd = cmd;
let spawnArgs = args;
let spawnArgs = licensesArgs;
const rosettaX64 = isDarwin && arch === "x64" && process.arch === "arm64" && !process.env.PYTHON_CMD;
if (rosettaX64) {
spawnCmd = "arch";
spawnArgs = ["-x86_64", cmd, ...licensesArgs];
}
console.log("Generating embedded third-party license artifacts...");
const licensesResult = spawnSync(spawnCmd, spawnArgs, {
stdio: "inherit",
shell: false,
env: env,
});
if (licensesResult.error) {
throw licensesResult.error;
}
if (licensesResult.status !== 0) {
process.exit(licensesResult.status || 1);
}
spawnCmd = cmd;
spawnArgs = args;
if (rosettaX64) {
spawnCmd = "arch";
spawnArgs = ["-x86_64", cmd, ...args];