""" Licensing: meshbay-common under the LGPL, everything else under the AGPL, and every third-party piece a build ships accounted for. Reads files and installed metadata; builds nothing. What it guards against is drift — a licence field nobody updates, a vendored file without its licence, a GPL dependency creeping into the one package that must stay usable under the LGPL. """ import importlib.util import json import re import tomllib from importlib import metadata from pathlib import Path ROOT = Path(__file__).resolve().parents[3] PACKAGES = ROOT / "packages" STATIC = PACKAGES / "meshbay-hub" / "src" / "meshbay_hub" / "static" VENDOR = STATIC / "vendor" DESKTOP = PACKAGES / "meshbay-client" / "src" ANDROID = PACKAGES / "meshbay-android" / "app" / "src" / "main" / "kotlin" KEYS = ANDROID / "org" / "meshbay" / "client" / "keys" SPDX_LGPL = "// SPDX-License-Identifier: LGPL-3.0-or-later\n" # The protocol layer in the clients (README, "Licence"): what a program needs to # speak to a hub and a node, under the LGPL in every language it exists in. LGPL_FILES = sorted( [STATIC / f for f in ("keyderive.js", "crypto.js", "playlist-crypto.js")] + [STATIC / "transport.js"] + sorted(STATIC.glob("transport-*.js")) + [DESKTOP / f for f in ("keyring.js", "transcripts.js", "argon2-wasm.js")] + [KEYS / f for f in ("Kdf.kt", "Keyring.kt", "Transcripts.kt")] ) EXPECTED = { "meshbay-common": ("LGPL-3.0-or-later", ["COPYING", "COPYING.LESSER"]), "meshbay-hub": ("AGPL-3.0-or-later", ["LICENSE"]), "meshbay-node": ("AGPL-3.0-or-later", ["LICENSE"]), } def _notices(): spec = importlib.util.spec_from_file_location( "third_party_notices", ROOT / "packaging" / "third_party_notices.py" ) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod def test_each_python_package_declares_its_licence_and_ships_the_text(): for pkg, (expr, files) in EXPECTED.items(): project = tomllib.loads((PACKAGES / pkg / "pyproject.toml").read_text())["project"] assert project["license"] == expr, pkg assert project["license-files"] == files, pkg for f in files: assert (PACKAGES / pkg / f).is_file(), f"{pkg}/{f}" def test_licence_texts_are_the_right_ones(): agpl = (ROOT / "LICENSE").read_text() assert "GNU AFFERO GENERAL PUBLIC LICENSE" in agpl and "Version 3" in agpl # Copies, because a wheel's license-files cannot reach outside its package. for pkg in ("meshbay-hub", "meshbay-node"): assert (PACKAGES / pkg / "LICENSE").read_text() == agpl, pkg common = PACKAGES / "meshbay-common" assert "GNU LESSER GENERAL PUBLIC LICENSE" in (common / "COPYING.LESSER").read_text() assert "GNU GENERAL PUBLIC LICENSE" in (common / "COPYING").read_text() def test_rpm_specs_and_the_client_agree_with_the_packages(): for pkg in ("meshbay-common", "meshbay-hub", "meshbay-node", "meshbay-client"): spec = (ROOT / "packaging" / "rpm" / f"{pkg}.spec").read_text() want = EXPECTED.get(pkg, ("AGPL-3.0-or-later",))[0] assert re.search(rf"^License:\s+{re.escape(want)}\s*$", spec, re.M), pkg assert "%license %{_licensedir}/%{name}" in spec, pkg pkg_json = json.loads((PACKAGES / "meshbay-client" / "package.json").read_text()) assert pkg_json["license"] == "AGPL-3.0-or-later" def test_every_windows_target_ships_the_licence(): pkg_json = json.loads((PACKAGES / "meshbay-client" / "package.json").read_text()) assert {"from": "../../LICENSE", "to": "LICENSE.txt"} in pkg_json["build"]["win"][ "extraResources" ] for yml in ("electron-builder.light.yml", "electron-builder.msix.yml"): text = (ROOT / "packaging" / "win" / yml).read_text() assert "- from: ../../LICENSE\n to: LICENSE.txt" in text, yml ps1 = (ROOT / "packaging" / "win" / "build-node-runtime.ps1").read_text() assert "third_party_notices.py" in ps1 and "THIRD-PARTY-NOTICES.txt" in ps1 def test_common_depends_on_nothing_copyleft(): """The LGPL is only worth something if the library can be taken alone.""" for req in metadata.distribution("meshbay-common").requires or []: if "extra ==" in req: continue name = re.split(r"[\s\[<>=!~;(]", req, maxsplit=1)[0] md = metadata.distribution(name).metadata label = " ".join( [md.get("License-Expression") or "", md.get("License") or ""] + (md.get_all("Classifier") or []) ) assert "GPL" not in label, f"{name}: {label[:120]}" def test_notices_follow_what_the_node_actually_ships(): mod = _notices() dists = mod._closure(["meshbay-node"]) assert "mutagen" in dists and "guessit" in dists and "av" in dists # Extras the node does not ask for, and dev tools, stay out. assert "pytest" not in dists and "piexif" not in dists text = mod.render(["meshbay-node"], [], with_python=False) assert re.search(r"^ mutagen [\d.]+ — GPL", text, re.M) libs = mod._native_libs(dists["av"]) if libs: # PyAV's FFmpeg is grafted in; the notice must say which assert libs[0] in text def test_every_vendored_file_has_its_provenance_and_licence(): provenance = (VENDOR / "PROVENANCE.md").read_text() licences = (VENDOR / "LICENSES.txt").read_text() for f in VENDOR.iterdir(): if f.name in ("PROVENANCE.md", "LICENSES.txt"): continue assert f"## {f.name}" in provenance or f"### {f.name}" in provenance, f.name assert f.name in licences, f.name def _sources(): for tree, pattern in ((STATIC, "*.js"), (DESKTOP, "*.js"), (ANDROID, "**/*.kt")): for f in tree.glob(pattern): if "vendor" not in f.parts and "locales" not in f.parts: yield f def test_the_lgpl_files_are_exactly_the_ones_that_say_so(): marked = sorted(f for f in _sources() if f.read_text().startswith(SPDX_LGPL)) assert marked == LGPL_FILES def test_every_client_carries_the_licence_texts(): """static/ is the interface of the web, the desktop and Android alike.""" texts = STATIC / "licenses" assert (texts / "AGPL-3.0.txt").read_text() == (ROOT / "LICENSE").read_text() common = PACKAGES / "meshbay-common" assert (texts / "LGPL-3.0.txt").read_text() == (common / "COPYING.LESSER").read_text() assert (texts / "GPL-3.0.txt").read_text() == (common / "COPYING").read_text() def _strip_js(src: str) -> str: src = re.sub(r"/\*[\s\S]*?\*/|//[^\n]*", "", src) return re.sub(r"'(?:\\.|[^'\\\n])*'|\"(?:\\.|[^\"\\\n])*\"", "''", src) def test_the_lgpl_layer_depends_on_nothing_under_the_agpl(): """ One import of an AGPL module and a client built on the layer is under the AGPL after all. What a host supplies (window.MeshBayPlatform, a Secrets store) is an injected interface, and is not looked for here. """ lgpl = set(LGPL_FILES) top = re.compile( r"^(?:export\s+)?(?:async\s+)?(?:function\*?\s+|(?:const|let|var|class)\s+)" r"([A-Za-z_$][\w$]*)", re.M, ) defined_in_lgpl = {n for f in lgpl if f.suffix == ".js" for n in top.findall(f.read_text())} agpl_globals = { n: f.name for f in STATIC.glob("*.js") if f not in lgpl for n in top.findall(f.read_text()) if n not in defined_in_lgpl } kt_decl = re.compile(r"^\s*(?:\w+\s+)*(?:class|object|interface)\s+(\w+)", re.M) defined_in_lgpl_kt = { n for f in lgpl if f.suffix == ".kt" for n in kt_decl.findall(f.read_text()) } agpl_kotlin = { n: f.name for f in ANDROID.glob("**/*.kt") if f not in lgpl for n in kt_decl.findall(f.read_text()) if n not in defined_in_lgpl_kt } for f in LGPL_FILES: src = f.read_text() if f.suffix == ".kt": for imp in re.findall(r"^import (org\.meshbay\.[\w.]+)", src, re.M): owner = ( imp.split(".")[-2] if imp.split(".")[-1][0].islower() else imp.split(".")[-1] ) assert owner in {g.stem for g in lgpl}, f"{f.name} imports {imp}" code = _strip_js(src) # Kotlin's comments and strings take the same shapes for name, owner in agpl_kotlin.items(): assert not re.search(rf"\b{name}\b", code), f"{f.name} uses {name} ({owner})" continue code = _strip_js(src) uncommented = re.sub(r"/\*[\s\S]*?\*/|^\s*//[^\n]*", "", src, flags=re.M) for spec in re.findall( r"""(?:\bfrom|\bimport\(|\brequire\()\s*['"]([^'"\n]+)['"]""", uncommented ): if spec.startswith("node:") or "vendor" in spec: continue assert (f.parent / spec).resolve() in lgpl, f"{f.name} imports {spec}" for name, owner in agpl_globals.items(): assert not re.search(rf"(? set[str]: """The modules the permission names — read from it, the one place they are listed.""" text = APP_EXCEPTION.read_text() block = text.split("2. the names exported by these modules", 1)[1].split("3.", 1)[0] return set(re.findall(r"^\s+([\w-]+\.js)\s*$", block, re.M)) def test_the_application_interface_names_modules_that_exist(): modules = _interface_modules() assert modules == {"i18n.js", "icon.js", "file-utils.js", "settings-ui.js", "folder-tree.js"} for m in modules: assert (STATIC / m).is_file(), m assert not (STATIC / m).read_text().startswith(SPDX_LGPL), ( f"{m} is LGPL already; the permission is for the AGPL part" ) def test_the_reference_application_is_free_to_copy_and_stays_inside_the_interface(): """ Copying helloworld is how an application starts. Were it to import anything outside the application interface, every application started from it would be a work based on the AGPL interface without anybody having chosen that. """ allowed = _interface_modules() | {f.name for f in LGPL_FILES if f.parent == STATIC} for f in REFERENCE_APP: src = f.read_text() assert src.startswith("// SPDX-License-Identifier: 0BSD\n"), f.name for spec in re.findall(r"""^import .* from ['"]\./([^'"]+)['"]""", src, re.M): assert spec.startswith("vendor/") or spec in allowed, f"{f.name} imports {spec}" assert "import(" not in _strip_js(src), f"{f.name}: a dynamic import escapes this check" def test_every_spdx_line_is_one_of_the_known_licences(): for f in _sources(): first = f.read_text().split("\n", 1)[0] if "SPDX-License-Identifier" not in first: continue if f in REFERENCE_APP: assert first.endswith(": 0BSD"), f.name else: assert f in LGPL_FILES, f"{f.name}: {first}"