From c71ae17535d1c6b9712c910689c022d98afa0c13 Mon Sep 17 00:00:00 2001 From: Ivan Date: Wed, 15 Apr 2026 03:11:13 -0500 Subject: [PATCH] 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 --- cx_setup.py | 12 ++ .../src/backend/data/THIRD_PARTY_NOTICES.txt | 91 +++++++++++++ meshchatx/src/backend/licenses_collector.py | 122 ++++++++++++++++-- scripts/build-backend.js | 26 +++- 4 files changed, 238 insertions(+), 13 deletions(-) create mode 100644 meshchatx/src/backend/data/THIRD_PARTY_NOTICES.txt diff --git a/cx_setup.py b/cx_setup.py index 76aea6e..ad59ac3 100644 --- a/cx_setup.py +++ b/cx_setup.py @@ -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")) diff --git a/meshchatx/src/backend/data/THIRD_PARTY_NOTICES.txt b/meshchatx/src/backend/data/THIRD_PARTY_NOTICES.txt new file mode 100644 index 0000000..61d1beb --- /dev/null +++ b/meshchatx/src/backend/data/THIRD_PARTY_NOTICES.txt @@ -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 +attrs 26.1.0 + License: MIT + Author: Hynek Schlawack +audioop-lts 0.2.2 + License: PSF-2.0 + Author: — +bcrypt 5.0.0 + License: Apache-2.0 + Author: The Python Cryptographic Authority developers +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 +frozenlist 1.8.0 + License: Apache-2.0 + Author: aiohttp team +idna 3.11 + License: BSD-3-Clause + Author: Kim Davies +jaraco.context 6.1.2 + License: MIT + Author: "Jason R. Coombs" +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 +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 +yarl 1.23.0 + License: Apache-2.0 + Author: Andrew Svetlov + +Node dependencies +----------------- +No entries. diff --git a/meshchatx/src/backend/licenses_collector.py b/meshchatx/src/backend/licenses_collector.py index b2acab1..e084fc3 100644 --- a/meshchatx/src/backend/licenses_collector.py +++ b/meshchatx/src/backend/licenses_collector.py @@ -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()) diff --git a/scripts/build-backend.js b/scripts/build-backend.js index 3f9f3db..5bd7b83 100755 --- a/scripts/build-backend.js +++ b/scripts/build-backend.js @@ -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];