1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
|
"""
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"(?<![\w$.]){re.escape(name)}\s*\(", code), (
f"{f.name} calls {name}() from {owner}"
)
APP_EXCEPTION = STATIC / "licenses" / "APPLICATION-EXCEPTION.txt"
REFERENCE_APP = [STATIC / "helloworld-app.js", STATIC / "helloworld-app-settings.js"]
def _interface_modules() -> 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}"
|