aboutsummaryrefslogtreecommitdiffstats
path: root/packaging
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-05 08:59:06 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-05 09:20:38 +0200
commitcce8a911553597ada33e275bc9b29fd34121074d (patch)
tree58e2eddfe4f0535e5d177959d8d6ea6da15cc552 /packaging
parentbacab81915a9ab640437b7d674bf9e29e701b1f1 (diff)
downloadmeshbay-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-xpackaging/build/build-common.sh5
-rwxr-xr-xpackaging/build/build-packages.sh35
-rw-r--r--packaging/rpm/meshbay-client.spec4
-rw-r--r--packaging/rpm/meshbay-common.spec8
-rw-r--r--packaging/rpm/meshbay-hub.spec4
-rw-r--r--packaging/rpm/meshbay-node.spec4
-rw-r--r--packaging/third_party_notices.py198
-rw-r--r--packaging/win/build-node-runtime.ps116
-rw-r--r--packaging/win/electron-builder.light.yml4
-rw-r--r--packaging/win/electron-builder.msix.yml4
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