License peo4c under GPL-3.0-or-later and ship third-party notices

- Add the GPL-3.0 LICENSE and SPDX headers to peo4c's own files.
- Add THIRD_PARTY_NOTICES.md listing every bundled component, its
  license and where its license text lives in the image, plus a
  trademark disclaimer; reference it from the README.
- Ship neo4j-browser's LICENSE, NOTICE.txt and LICENSES.txt with a
  source archive of the built commit, as the GPL requires.
- Ship Apache AGE's LICENSE and NOTICE, which the base image lacks.
- Collect license texts for Python packages whose wheels omit them,
  from PyPI sdists or vendored copies in docker/licenses/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Bart BroereandClaude Opus 5.5 committed 2026-10-14 11:58:27 +02:00
1 parent e72af59401
commit 04e1c32223
22 files changed
+1152 -1

No files matched your search

+84
View File
@@ -0,0 +1,84 @@
# SPDX-FileCopyrightText: 2026 Bart Broere
# SPDX-License-Identifier: GPL-3.0-or-later
"""Make sure every Python distribution in an environment ships its license text.
Most wheels include their license files in ``*.dist-info``. For the ones that
do not, copy its license files to ``<output>/<name>-<version>/``, taking them
from ``<vendored>/<name>/`` when present and otherwise from the source
distribution on PyPI. Run with the interpreter of the environment to inspect:
``/opt/pgadmin/bin/python collect-python-licenses.py <output> <vendored>``.
"""
import io
import json
import re
import sys
import tarfile
import urllib.request
import zipfile
from importlib import metadata
from pathlib import Path
LICENSE_FILE = re.compile(r"(^|/)(LICEN[CS]E|COPYING|NOTICE|AUTHORS)[^/]*$", re.IGNORECASE)
def has_license_file(dist):
return any(LICENSE_FILE.search(str(f)) for f in dist.files or [])
def sdist_members(name, version):
with urllib.request.urlopen(f"https://pypi.org/pypi/{name}/{version}/json", timeout=60) as r:
urls = json.load(r)["urls"]
sdist = next((u for u in urls if u["packagetype"] == "sdist"), None)
if sdist is None:
return {}
with urllib.request.urlopen(sdist["url"], timeout=120) as r:
data = r.read()
found = {}
if sdist["filename"].endswith(".zip"):
with zipfile.ZipFile(io.BytesIO(data)) as z:
for n in z.namelist():
if n.count("/") == 1 and LICENSE_FILE.search(n):
found[n.split("/", 1)[1]] = z.read(n)
else:
with tarfile.open(fileobj=io.BytesIO(data)) as t:
for m in t.getmembers():
if m.isfile() and m.name.count("/") == 1 and LICENSE_FILE.search(m.name):
found[m.name.split("/", 1)[1]] = t.extractfile(m).read()
return found
def vendored_files(vendored, name):
directory = vendored / name
if not directory.is_dir():
return {}
return {f.name: f.read_bytes() for f in directory.iterdir() if f.is_file()}
def main(output, vendored):
output, vendored = Path(output), Path(vendored)
missing = []
for dist in sorted(metadata.distributions(), key=lambda d: d.metadata["Name"].lower()):
if has_license_file(dist):
continue
name, version = dist.metadata["Name"], dist.version
files = vendored_files(vendored, name)
if not files:
try:
files = sdist_members(name, version)
except Exception as e: # noqa: BLE001 - report and carry on
print(f"{name} {version}: could not fetch sdist: {e}", file=sys.stderr)
if not files:
missing.append(f"{name} {version}")
continue
target = output / f"{name}-{version}"
target.mkdir(parents=True, exist_ok=True)
for filename, content in files.items():
(target / filename).write_bytes(content)
print(f"{name} {version}: added {', '.join(files)}")
for item in missing:
print(f"WARNING: no license file found for {item}", file=sys.stderr)
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2])