diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-05 08:59:06 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-05 09:20:38 +0200 |
| commit | cce8a911553597ada33e275bc9b29fd34121074d (patch) | |
| tree | 58e2eddfe4f0535e5d177959d8d6ea6da15cc552 /packaging | |
| parent | bacab81915a9ab640437b7d674bf9e29e701b1f1 (diff) | |
| download | meshbay-cce8a911553597ada33e275bc9b29fd34121074d.tar.gz | |
chore: license MeshBay — LGPL protocol layer, AGPL for the rest
The protocol layer is LGPL-3.0-or-later in every language it exists in, so
any client may use it whatever its own licence: meshbay-common, and the files
marked with an SPDX line — keyderive.js, crypto.js, playlist-crypto.js,
transport*.js; keyring.js, transcripts.js and argon2-wasm.js on the desktop;
Kdf.kt, Keyring.kt and Transcripts.kt on Android. Everything else is
AGPL-3.0-or-later, which the RPM specs and package.json already declared
without a licence file to back them.
Two AGPL section 7 permissions:
- group applications may be under any licence when they use the interface
only through a named surface (static/licenses/APPLICATION-EXCEPTION.txt);
the reference application is 0BSD so that copying it brings no AGPL code;
- the Android application may be conveyed linked with Google Play services.
Third-party code is accounted for: THIRD-PARTY-NOTICES.txt is generated from
what a build ships (packaging/third_party_notices.py) for the deb/rpm venv and
the frozen Windows node — PyAV's wheel grafts in libx264 and libx265, which its
BSD licence does not mention — and the vendored browser libraries get their
licence texts and htm-preact.js its provenance. Wheels carry SPDX metadata,
RPMs %license, debs a DEP-5 copyright file, every Windows target LICENSE.txt.
test_licensing.py holds the line: the LGPL layer imports nothing under the
AGPL, the reference application nothing outside the application interface,
and every SPDX line is one of the known ones.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packaging')
| -rwxr-xr-x | packaging/build/build-common.sh | 5 | ||||
| -rwxr-xr-x | packaging/build/build-packages.sh | 35 | ||||
| -rw-r--r-- | packaging/rpm/meshbay-client.spec | 4 | ||||
| -rw-r--r-- | packaging/rpm/meshbay-common.spec | 8 | ||||
| -rw-r--r-- | packaging/rpm/meshbay-hub.spec | 4 | ||||
| -rw-r--r-- | packaging/rpm/meshbay-node.spec | 4 | ||||
| -rw-r--r-- | packaging/third_party_notices.py | 198 | ||||
| -rw-r--r-- | packaging/win/build-node-runtime.ps1 | 16 | ||||
| -rw-r--r-- | packaging/win/electron-builder.light.yml | 4 | ||||
| -rw-r--r-- | packaging/win/electron-builder.msix.yml | 4 |
10 files changed, 278 insertions, 4 deletions
diff --git a/packaging/build/build-common.sh b/packaging/build/build-common.sh index 3045689..5f119c6 100755 --- a/packaging/build/build-common.sh +++ b/packaging/build/build-common.sh @@ -44,6 +44,11 @@ echo " installing all packages + dependencies" --find-links "$WHEEL_DIR" \ meshbay-common meshbay-hub meshbay-node 2>&1 | tail -3 +# --- Third-party notices: every package the venv ships, with its licence ------- +echo " writing THIRD-PARTY-NOTICES.txt" +"$VENV_BUILD/bin/python" "$REPO/packaging/third_party_notices.py" \ + -o "$ROOT/opt/meshbay-common/THIRD-PARTY-NOTICES.txt" meshbay-hub meshbay-node + # --- Strip build tools from the venv (not needed at runtime) --------------- echo " stripping build tools" "$VENV_BUILD/bin/pip" uninstall -y pip setuptools wheel 2>&1 | tail -1 diff --git a/packaging/build/build-packages.sh b/packaging/build/build-packages.sh index ccacb81..8ca0e23 100755 --- a/packaging/build/build-packages.sh +++ b/packaging/build/build-packages.sh @@ -84,6 +84,38 @@ echo "" echo "--- Packaging ($FORMAT) ---" if [ "$FORMAT" = "deb" ]; then + # Debian policy: /usr/share/doc/<pkg>/copyright, machine-readable. The LGPL + # is in /usr/share/common-licenses and is referred to; the AGPL is not, so + # its full text goes in, as a DEP-5 licence paragraph (indented, "." for a + # blank line). + install_copyright() { + local pkg="$1" root="$2" + local doc="$root/usr/share/doc/$pkg" + mkdir -p "$doc" + { + echo "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/" + echo "Upstream-Name: MeshBay" + echo "Source: https://git.meshbay.org/" + echo "" + echo "Files: *" + echo "Copyright: MeshBay contributors" + if [ "$pkg" = "meshbay-common" ]; then + echo "License: LGPL-3.0-or-later" + echo " On Debian systems, the full text of the GNU Lesser General Public" + echo " License version 3 is in /usr/share/common-licenses/LGPL-3, and the" + echo " GNU General Public License it builds on in /usr/share/common-licenses/GPL-3." + echo " ." + echo " The venv under /opt/meshbay-common carries the Python packages MeshBay" + echo " depends on, each under its own licence; they are listed, with their" + echo " licence texts, in /opt/meshbay-common/THIRD-PARTY-NOTICES.txt." + else + echo "License: AGPL-3.0-or-later" + sed -e 's/^$/./' -e 's/^/ /' "$REPO/LICENSE" + fi + } > "$doc/copyright" + chmod 644 "$doc/copyright" + } + build_deb() { local pkg="$1" local root="$STAGING/${pkg}-root" @@ -102,6 +134,8 @@ if [ "$FORMAT" = "deb" ]; then [ -f "$deb_dir/control" ] && chmod 644 "$deb_dir/control" [ -f "$deb_dir/conffiles" ] && chmod 644 "$deb_dir/conffiles" + install_copyright "$pkg" "$root" + dpkg-deb --build --root-owner-group "$root" "$OUT/${pkg}_${VERSION}_${ARCH}.deb" echo " -> $OUT/${pkg}_${VERSION}_${ARCH}.deb" } @@ -127,6 +161,7 @@ elif [ "$FORMAT" = "rpm" ]; then rpmbuild \ --define "_topdir $RPMBUILD_DIR" \ --define "_staging_root $root" \ + --define "_repo_root $REPO" \ -bb "$RPMBUILD_DIR/SPECS/${pkg}.spec" 2>&1 | tail -5 local rpm_file diff --git a/packaging/rpm/meshbay-client.spec b/packaging/rpm/meshbay-client.spec index 0816741..e4f9f03 100644 --- a/packaging/rpm/meshbay-client.spec +++ b/packaging/rpm/meshbay-client.spec @@ -2,7 +2,7 @@ Name: meshbay-client Version: __VERSION__ Release: 1%{?dist} Summary: MeshBay — peer-to-peer file sharing, streaming and group chat -License: AGPLv3+ +License: AGPL-3.0-or-later URL: https://meshbay.org AutoReqProv: no @@ -35,6 +35,7 @@ launcher named "MeshBay". %install cp -a %{_staging_root}/* %{buildroot}/ +install -Dm644 -t %{buildroot}%{_licensedir}/%{name} %{_repo_root}/LICENSE %post if [ -f /opt/meshbay-client/chrome-sandbox ]; then @@ -49,6 +50,7 @@ update-desktop-database /usr/share/applications 2>/dev/null || true gtk-update-icon-cache -f -t /usr/share/icons/hicolor 2>/dev/null || true %files +%license %{_licensedir}/%{name} /opt/meshbay-client /usr/bin/meshbay /usr/share/applications/meshbay.desktop diff --git a/packaging/rpm/meshbay-common.spec b/packaging/rpm/meshbay-common.spec index 147dc17..cad00b7 100644 --- a/packaging/rpm/meshbay-common.spec +++ b/packaging/rpm/meshbay-common.spec @@ -2,7 +2,9 @@ Name: meshbay-common Version: __VERSION__ Release: 1%{?dist} Summary: MeshBay shared Python runtime and libraries -License: AGPLv3+ +# The venv carries every Python dependency, each under its own licence: +# /opt/meshbay-common/THIRD-PARTY-NOTICES.txt lists them. +License: LGPL-3.0-or-later URL: https://meshbay.org AutoReqProv: no @@ -16,6 +18,9 @@ Installs to /opt/meshbay-common/venv/. %install cp -a %{_staging_root}/* %{buildroot}/ +install -Dm644 -t %{buildroot}%{_licensedir}/%{name} \ + %{_repo_root}/packages/meshbay-common/COPYING \ + %{_repo_root}/packages/meshbay-common/COPYING.LESSER # Bytecode written at run time into __pycache__ is not the package's, and would # keep rpm from removing a directory the new version no longer ships. @@ -32,6 +37,7 @@ if [ "$1" -eq 0 ]; then fi %files +%license %{_licensedir}/%{name} /opt/meshbay-common %changelog diff --git a/packaging/rpm/meshbay-hub.spec b/packaging/rpm/meshbay-hub.spec index ffd991a..141f6e6 100644 --- a/packaging/rpm/meshbay-hub.spec +++ b/packaging/rpm/meshbay-hub.spec @@ -2,7 +2,7 @@ Name: meshbay-hub Version: __VERSION__ Release: 1%{?dist} Summary: MeshBay Hub — identity authority and group registry server -License: AGPLv3+ +License: AGPL-3.0-or-later URL: https://meshbay.org AutoReqProv: no @@ -21,6 +21,7 @@ Runs as a systemd service behind Caddy for HTTPS. %install cp -a %{_staging_root}/* %{buildroot}/ +install -Dm644 -t %{buildroot}%{_licensedir}/%{name} %{_repo_root}/LICENSE %pre getent group meshbay >/dev/null || groupadd -r meshbay @@ -51,6 +52,7 @@ fi %systemd_postun_with_restart meshbay-hub.service %files +%license %{_licensedir}/%{name} /opt/meshbay-hub /opt/meshbay-common/venv/lib/python*/site-packages/meshbay_hub/ /opt/meshbay-common/venv/lib/python*/site-packages/meshbay_hub-*.dist-info/ diff --git a/packaging/rpm/meshbay-node.spec b/packaging/rpm/meshbay-node.spec index 89f3a0a..18d432e 100644 --- a/packaging/rpm/meshbay-node.spec +++ b/packaging/rpm/meshbay-node.spec @@ -2,7 +2,7 @@ Name: meshbay-node Version: __VERSION__ Release: 1%{?dist} Summary: MeshBay Node — local file host, streaming server, and group daemon -License: AGPLv3+ +License: AGPL-3.0-or-later URL: https://meshbay.org AutoReqProv: no @@ -29,6 +29,7 @@ Runs as a systemd user service. %install cp -a %{_staging_root}/* %{buildroot}/ +install -Dm644 -t %{buildroot}%{_licensedir}/%{name} %{_repo_root}/LICENSE %post if [ -d /run/systemd/system ]; then @@ -90,6 +91,7 @@ if [ "$1" -eq 0 ]; then fi %files +%license %{_licensedir}/%{name} /opt/meshbay-node /opt/meshbay-common/venv/lib/python*/site-packages/meshbay_node/ /opt/meshbay-common/venv/lib/python*/site-packages/meshbay_node-*.dist-info/ diff --git a/packaging/third_party_notices.py b/packaging/third_party_notices.py new file mode 100644 index 0000000..85a35a9 --- /dev/null +++ b/packaging/third_party_notices.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""Write THIRD-PARTY-NOTICES.txt for the Python packages a MeshBay build ships. + +Run with the interpreter of the environment being shipped — the deb/rpm venv +(build-common.sh) or the PyInstaller build venv (build-node-runtime.ps1) — so +the list is the set actually installed there, read from each package's own +metadata, rather than a hand-kept list that drifts with every upgrade. + + python third_party_notices.py -o OUT ROOT... [--extra NAME...] [--with-python] + +ROOTS are walked through their runtime requirements (extras skipped). --extra +names packages that ship without being imported, PyInstaller's bootloader being +the case. Native libraries a wheel grafts into a `<name>.libs/` directory are +listed under the package that carries them: PyAV's FFmpeg build includes +libx264 and libx265, both GPL, and that is not visible in its own BSD licence. +""" + +import argparse +import re +import sys +from importlib import metadata +from pathlib import Path + +OWN = re.compile(r"^meshbay-") +LICENSE_NAME = re.compile(r"(LICEN[CS]E|COPYING|NOTICE|AUTHORS)", re.IGNORECASE) +RULE = "=" * 78 + + +def _norm(name: str) -> str: + return re.sub(r"[-_.]+", "-", name).lower() + + +def _marker_applies(marker: str, extras: set[str]) -> bool: + try: + from packaging.markers import Marker + except ImportError: + # Better a notice too many than one missing. + return "extra" not in marker or any(f'"{e}"' in marker for e in extras) + return any(Marker(marker).evaluate({"extra": e}) for e in extras | {""}) + + +def _parse(req: str) -> tuple[str, set[str], str]: + spec, _, marker = req.partition(";") + m = re.match(r"\s*([A-Za-z0-9._-]+)\s*(?:\[([^\]]*)\])?", spec) + extras = {_norm(e) for e in (m.group(2) or "").split(",") if e.strip()} + return m.group(1), extras, marker.strip() + + +def _closure(roots: list[str]) -> dict[str, metadata.Distribution]: + seen: dict[str, metadata.Distribution] = {} + done: set[tuple[str, str]] = set() + todo = [_parse(r)[:2] for r in roots] + while todo: + name, extras = todo.pop() + name = _norm(name) + try: + dist = seen.get(name) or metadata.distribution(name) + except metadata.PackageNotFoundError: + continue # a requirement whose marker excludes this platform + seen[name] = dist + for extra in extras | {""}: + if (name, extra) in done: + continue + done.add((name, extra)) + for req in dist.requires or []: + dep, dep_extras, marker = _parse(req) + if marker and not _marker_applies(marker, {extra} - {""}): + continue + if not marker and extra: + continue # already taken with the base requirements + todo.append((dep, dep_extras)) + return seen + + +def _license_label(dist: metadata.Distribution) -> str: + md = dist.metadata + expr = md.get("License-Expression") + if expr: + return expr + classifiers = [ + c.split("::")[-1].strip() + for c in md.get_all("Classifier") or [] + if c.startswith("License ::") + ] + if classifiers: + return "; ".join(classifiers) + return (md.get("License") or "see licence text below").splitlines()[0] + + +def _license_texts(dist: metadata.Distribution) -> list[tuple[str, str]]: + texts = [] + for f in dist.files or []: + parts = f.parts + if not parts or not parts[0].endswith(".dist-info"): + continue + if not LICENSE_NAME.search(f.name) or f.suffix in (".py", ".pyc"): + continue + try: + texts.append(("/".join(parts[1:]), f.read_text(encoding="utf-8"))) + except (OSError, UnicodeDecodeError): + continue + return texts + + +def _native_libs(dist: metadata.Distribution) -> list[str]: + return sorted( + f.name for f in dist.files or [] if len(f.parts) > 1 and f.parts[0].endswith(".libs") + ) + + +def _homepage(dist: metadata.Distribution) -> str: + md = dist.metadata + if md.get("Home-page"): + return md["Home-page"] + for url in md.get_all("Project-URL") or []: + label, _, link = url.partition(",") + if label.strip().lower() in ("homepage", "source", "repository", "source code"): + return link.strip() + return "" + + +def render(roots: list[str], extra: list[str], with_python: bool) -> str: + dists = _closure(roots + extra) + own = sorted(n for n in dists if OWN.match(n)) + third = sorted(n for n in dists if not OWN.match(n)) + + out = [ + "MeshBay — third-party notices", + RULE, + "", + "MeshBay itself: meshbay-common is LGPL-3.0-or-later, every other MeshBay", + "component is AGPL-3.0-or-later. Source: https://git.meshbay.org/", + "", + "This build also carries the packages below, each under its own licence.", + "Each is distributed unmodified, as published on https://pypi.org/; the", + "corresponding source of every one is that release's source distribution", + "there, or the project home page given with it.", + "", + ] + if with_python: + out += [f"Python {sys.version.split()[0]} — PSF-2.0 — https://www.python.org/", ""] + for name in own: + out.append( + f" {dists[name].metadata['Name']} {dists[name].version}" + f" — {_license_label(dists[name])}" + ) + out.append("") + for name in third: + d = dists[name] + out.append(f" {d.metadata['Name']} {d.version} — {_license_label(d)}") + out.append("") + + for name in third: + d = dists[name] + out += [RULE, f"{d.metadata['Name']} {d.version}", f"Licence: {_license_label(d)}"] + if home := _homepage(d): + out.append(f"Home: {home}") + if libs := _native_libs(d): + out.append("Native libraries bundled in this package's wheel (each under its") + out.append("own licence, built and published by the project above):") + out += [f" {lib}" for lib in libs] + out.append(RULE) + texts = _license_texts(d) + if not texts: + out.append("(no licence file shipped in this package's metadata)") + for path, text in texts: + out += ["", f"--- {path} ---", "", text.rstrip(), ""] + out.append("") + + base_license = Path(sys.base_prefix) / "LICENSE.txt" + if with_python and base_license.is_file(): + out += [ + RULE, + f"Python {sys.version.split()[0]}", + RULE, + "", + base_license.read_text(encoding="utf-8", errors="replace").rstrip(), + "", + ] + return "\n".join(out) + "\n" + + +def main() -> None: + ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + ap.add_argument("-o", "--output", type=Path, required=True) + ap.add_argument("roots", nargs="+") + ap.add_argument("--extra", nargs="*", default=[]) + ap.add_argument( + "--with-python", + action="store_true", + help="the interpreter itself ships too (a frozen build, not a system-python venv)", + ) + args = ap.parse_args() + args.output.write_text(render(args.roots, args.extra, args.with_python), encoding="utf-8") + + +if __name__ == "__main__": + main() diff --git a/packaging/win/build-node-runtime.ps1 b/packaging/win/build-node-runtime.ps1 index 371311b..1171146 100644 --- a/packaging/win/build-node-runtime.ps1 +++ b/packaging/win/build-node-runtime.ps1 @@ -109,6 +109,22 @@ if (-not (Test-Path (Join-Path $frozen "meshbay-node.exe"))) { throw "PyInstaller did not produce meshbay-node.exe at $frozen" } +# --- 3b. licences ---------------------------------------------------- +# The frozen tree is a distribution of every package in it, so each one's +# licence goes with it: MeshBay's own (AGPL for the node, LGPL for the common +# library it embeds) and THIRD-PARTY-NOTICES.txt, generated from the build +# venv's own metadata -- PyAV's wheel, for one, grafts in a GPL FFmpeg build +# (libx264, libx265) that its BSD licence does not mention. PyInstaller's +# bootloader and the interpreter ship too, without being a requirement. +Step "writing licence notices" +Copy-Item (Join-Path $Repo "LICENSE") (Join-Path $frozen "LICENSE.txt") +Copy-Item (Join-Path $Repo "packages\meshbay-common\COPYING.LESSER") (Join-Path $frozen "LICENSE-meshbay-common.txt") +Copy-Item (Join-Path $Repo "packages\meshbay-common\COPYING") (Join-Path $frozen "LICENSE-GPL-3.0.txt") +& $Python (Join-Path $Repo "packaging\third_party_notices.py") ` + -o (Join-Path $frozen "THIRD-PARTY-NOTICES.txt") ` + meshbay-node tzdata --extra pyinstaller --with-python +if ($LASTEXITCODE -ne 0) { throw "third_party_notices.py failed" } + # --- 4. ffmpeg (bundled by default) ----------------------------------- if ($SkipFfmpeg -or $env:MESHBAY_SKIP_FFMPEG -eq "1") { Write-Host " !! ffmpeg not bundled (-SkipFfmpeg) -- the node will look for it on PATH, and streaming needs it installed separately" -ForegroundColor Yellow diff --git a/packaging/win/electron-builder.light.yml b/packaging/win/electron-builder.light.yml index 502a3c5..6cd2b0d 100644 --- a/packaging/win/electron-builder.light.yml +++ b/packaging/win/electron-builder.light.yml @@ -36,6 +36,10 @@ win: # ICE + the 2 LAN-casting rules) and logs the node rule as skipped. - from: ../../packaging/win/firewall.ps1 to: firewall.ps1 + # The application's own licence, beside the app (Electron's LICENSE and + # LICENSES.chromium.html are put next to the exe by electron-builder). + - from: ../../LICENSE + to: LICENSE.txt nsis: oneClick: false diff --git a/packaging/win/electron-builder.msix.yml b/packaging/win/electron-builder.msix.yml index 5460267..d3adad6 100644 --- a/packaging/win/electron-builder.msix.yml +++ b/packaging/win/electron-builder.msix.yml @@ -69,6 +69,10 @@ win: # anticipated in the original plan). - from: ../../packaging/win/ensure-node-path.ps1 to: ensure-node-path.ps1 + # The application's own licence, beside the app (Electron's LICENSE and + # LICENSES.chromium.html are put next to the exe by electron-builder). + - from: ../../LICENSE + to: LICENSE.txt appx: # --- Real values, from Partner Center's "App identity" page (App |